@@ -14,10 +14,10 @@ export interface BridgeAccessOptions {
1414export interface BridgeAccessResult {
1515 apiEndpoint : string ;
1616 accessId : string ;
17- /** Whether the access was newly created (vs. reused) */
17+ /** Whether the access was newly created (vs. reused/updated ) */
1818 created : boolean ;
19- /** Whether the access was recreated (old deleted, new created) */
20- recreated : boolean ;
19+ /** Whether an existing access was updated in place via accesses.update */
20+ updated : boolean ;
2121}
2222
2323/**
@@ -39,7 +39,7 @@ export async function getOrCreateBridgeAccess (
3939 apiEndpoint : existing . apiEndpoint ,
4040 accessId : existing . id ,
4141 created : false ,
42- recreated : false
42+ updated : false
4343 } ;
4444 }
4545
@@ -53,146 +53,90 @@ export async function getOrCreateBridgeAccess (
5353 apiEndpoint : access . apiEndpoint ,
5454 accessId : access . id ,
5555 created : true ,
56- recreated : false
57- } ;
58- }
59-
60- /**
61- * Recreate a bridge access with updated permissions/clientData.
62- * Deletes the old access and creates a new one, carrying forward previousAccessIds
63- * so that events created under old accesses are still attributable.
64- *
65- * @param connection - Pryv connection to the user's account (personal token)
66- * @param options - new access configuration
67- */
68- export async function recreateBridgeAccess (
69- connection : pryv . Connection ,
70- options : BridgeAccessOptions
71- ) : Promise < BridgeAccessResult > {
72- const accesses = await ( connection as any ) . apiOne ( 'accesses.get' , { } , 'accesses' ) ;
73- const existing = accesses . find ( ( a : any ) => a . name === options . name ) ;
74-
75- // Build previousAccessIds chain
76- const previousAccessIds : string [ ] = [ ] ;
77- if ( existing ) {
78- if ( existing . id ) previousAccessIds . push ( existing . id ) ;
79- const oldPrevIds = existing . clientData ?. previousAccessIds ;
80- if ( Array . isArray ( oldPrevIds ) ) {
81- for ( const id of oldPrevIds ) {
82- if ( ! previousAccessIds . includes ( id ) ) previousAccessIds . push ( id ) ;
83- }
84- }
85- // Delete old access
86- await ( connection as any ) . apiOne ( 'accesses.delete' , { id : existing . id } , 'accessDeletion' ) ;
87- logger . info ( 'Deleted old bridge access for recreation' , { name : options . name , oldId : existing . id } ) ;
88- }
89-
90- // Merge previousAccessIds into clientData
91- const clientData = {
92- ...options . clientData ,
93- previousAccessIds : previousAccessIds . length > 0 ? previousAccessIds : undefined
94- } ;
95-
96- const access = await ( connection as any ) . apiOne ( 'accesses.create' , {
97- name : options . name ,
98- permissions : options . permissions ,
99- clientData
100- } , 'access' ) ;
101-
102- return {
103- apiEndpoint : access . apiEndpoint ,
104- accessId : access . id ,
105- created : true ,
106- recreated : existing != null
56+ updated : false
10757 } ;
10858}
10959
11060/**
11161 * Get or create a bridge access, with optional permission update detection.
112- * If the access exists but permissions differ, recreates it with the new permissions
113- * while preserving previousAccessIds for event attribution.
62+ *
63+ * If the access exists and `updateIfDifferent` is set and permissions differ,
64+ * updates it in place via `accesses.update` (Plan 66). The access id becomes
65+ * composite (`<base>:<serial>`) but the token and apiEndpoint are preserved.
66+ *
67+ * Server-side `clientData` merge means any pre-existing keys on the access
68+ * (notably `previousAccessIds` from the legacy delete+create era) are
69+ * preserved automatically — we only send the keys we want to set.
70+ *
71+ * `StaleAccessIdError` handling: if another writer updates the access between
72+ * our `accesses.get` and our `accesses.update`, we refetch + retry once.
73+ * Two consecutive stale errors propagate.
11474 *
11575 * @param connection - Pryv connection to the user's account (personal token)
11676 * @param options - access configuration
117- * @param options.updateIfDifferent - if true, recreate when permissions differ (default: false)
77+ * @param options.updateIfDifferent - if true, update permissions in place when they differ (default: false)
11878 */
11979export async function ensureBridgeAccess (
12080 connection : pryv . Connection ,
12181 options : BridgeAccessOptions & { updateIfDifferent ?: boolean }
12282) : Promise < BridgeAccessResult > {
123- const accesses = await ( connection as any ) . apiOne ( 'accesses.get' , { } , 'accesses' ) ;
124- const existing = accesses . find ( ( a : any ) => a . name === options . name ) ;
125-
126- if ( existing ) {
127- // Check if permissions match
128- if ( options . updateIfDifferent && ! permissionsMatch ( existing . permissions , options . permissions ) ) {
129- logger . info ( 'Bridge access permissions differ, recreating' , { name : options . name } ) ;
130- // Can't re-fetch — pass existing directly to avoid double API call
131- return await _recreateFromExisting ( connection , existing , options ) ;
83+ let attempt = 0 ;
84+ while ( true ) {
85+ const accesses = await ( connection as any ) . apiOne ( 'accesses.get' , { } , 'accesses' ) ;
86+ const existing = accesses . find ( ( a : any ) => a . name === options . name ) ;
87+
88+ if ( ! existing ) {
89+ const access = await ( connection as any ) . apiOne ( 'accesses.create' , {
90+ name : options . name ,
91+ permissions : options . permissions ,
92+ clientData : options . clientData || { }
93+ } , 'access' ) ;
94+ return {
95+ apiEndpoint : access . apiEndpoint ,
96+ accessId : access . id ,
97+ created : true ,
98+ updated : false
99+ } ;
132100 }
133- return {
134- apiEndpoint : existing . apiEndpoint ,
135- accessId : existing . id ,
136- created : false ,
137- recreated : false
138- } ;
139- }
140-
141- const access = await ( connection as any ) . apiOne ( 'accesses.create' , {
142- name : options . name ,
143- permissions : options . permissions ,
144- clientData : options . clientData || { }
145- } , 'access' ) ;
146101
147- return {
148- apiEndpoint : access . apiEndpoint ,
149- accessId : access . id ,
150- created : true ,
151- recreated : false
152- } ;
153- }
102+ if ( ! options . updateIfDifferent || permissionsMatch ( existing . permissions , options . permissions ) ) {
103+ return {
104+ apiEndpoint : existing . apiEndpoint ,
105+ accessId : existing . id ,
106+ created : false ,
107+ updated : false
108+ } ;
109+ }
154110
155- /** @private recreate from an already-fetched existing access */
156- async function _recreateFromExisting (
157- connection : pryv . Connection ,
158- existing : any ,
159- options : BridgeAccessOptions
160- ) : Promise < BridgeAccessResult > {
161- const previousAccessIds : string [ ] = [ ] ;
162- if ( existing . id ) previousAccessIds . push ( existing . id ) ;
163- const oldPrevIds = existing . clientData ?. previousAccessIds ;
164- if ( Array . isArray ( oldPrevIds ) ) {
165- for ( const id of oldPrevIds ) {
166- if ( ! previousAccessIds . includes ( id ) ) previousAccessIds . push ( id ) ;
111+ // Update in place. Server merges clientData; we only send our new keys.
112+ const updatePayload : Record < string , any > = { permissions : options . permissions } ;
113+ if ( options . clientData != null ) updatePayload . clientData = options . clientData ;
114+
115+ try {
116+ logger . info ( 'Bridge access permissions differ, updating' , { name : options . name , id : existing . id } ) ;
117+ const updated = await ( connection as any ) . updateAccess ( existing . id , updatePayload ) ;
118+ return {
119+ apiEndpoint : updated . apiEndpoint ,
120+ accessId : updated . id ,
121+ created : false ,
122+ updated : true
123+ } ;
124+ } catch ( e : any ) {
125+ if ( e instanceof ( pryv as any ) . StaleAccessIdError && attempt === 0 ) {
126+ attempt ++ ;
127+ logger . info ( 'Bridge access stale on update, refetching and retrying once' , { name : options . name } ) ;
128+ continue ;
129+ }
130+ throw e ;
167131 }
168132 }
169-
170- await ( connection as any ) . apiOne ( 'accesses.delete' , { id : existing . id } , 'accessDeletion' ) ;
171-
172- const clientData = {
173- ...options . clientData ,
174- previousAccessIds : previousAccessIds . length > 0 ? previousAccessIds : undefined
175- } ;
176-
177- const access = await ( connection as any ) . apiOne ( 'accesses.create' , {
178- name : options . name ,
179- permissions : options . permissions ,
180- clientData
181- } , 'access' ) ;
182-
183- return {
184- apiEndpoint : access . apiEndpoint ,
185- accessId : access . id ,
186- created : true ,
187- recreated : true
188- } ;
189133}
190134
191135/** Compare two permission arrays (order-independent) */
192136function permissionsMatch ( a : any [ ] , b : Permission [ ] ) : boolean {
193137 if ( ! a || ! b ) return false ;
194138 if ( a . length !== b . length ) return false ;
195- const normalize = ( p : any ) => `${ p . streamId || '' } :${ p . level || '' } :${ p . feature || '' } :${ p . setting || '' } ` ;
139+ const normalize = ( p : any ) : string => `${ p . streamId || '' } :${ p . level || '' } :${ p . feature || '' } :${ p . setting || '' } ` ;
196140 const setA = new Set ( a . map ( normalize ) ) ;
197141 const setB = new Set ( b . map ( normalize ) ) ;
198142 if ( setA . size !== setB . size ) return false ;
0 commit comments