@kubun/plugin-connector 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/action.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { CredentialProvider, EntityRecord } from '@kubun/connector';
2
2
  import type { StoreProvider } from '@kubun/db';
3
+ import type { Logger } from '@kubun/logger';
3
4
  import type { ConnectorRegistry } from './registry.js';
5
+ import { type SignedDocumentWriter } from './sync/processor.js';
4
6
  export type ExecuteActionParams = {
5
7
  connector: string;
6
8
  action: string;
@@ -17,5 +19,13 @@ export type ExecuteActionDeps = {
17
19
  registry: ConnectorRegistry;
18
20
  credentialProvider: CredentialProvider;
19
21
  ownerDID: string;
22
+ logger: Logger;
23
+ /**
24
+ * Signs and applies the action's resulting document as `ownerDID` (the
25
+ * viewer) through the mutation pipeline. The action runs inside the request's
26
+ * mutation transaction, so this must use the in-transaction signed-set seam
27
+ * rather than opening a nested transaction.
28
+ */
29
+ writeDocument: SignedDocumentWriter;
20
30
  };
21
31
  export declare function executeAction(params: ExecuteActionParams, deps: ExecuteActionDeps): Promise<ExecuteActionResult>;
package/lib/action.js CHANGED
@@ -1 +1,106 @@
1
- import{DocumentID as e,DocumentModelID as r}from"@kubun/id";import{getGraphStore as o}from"@kubun/store-graph";import{EntityProcessor as t}from"./sync/processor.js";export async function executeAction(n,l){let c,i,{stores:a,registry:u,credentialProvider:s,ownerDID:d}=l,{connector:f,action:p,model:w,input:m,sourceID:E}=n,h=u.get(f);if(null==h)throw Error(`Connector "${f}" not found`);if(null==h.actionHandler)throw Error(`Connector "${f}" does not support actions`);if(null==h.actions?.find(e=>e.name===p))throw Error(`Action "${p}" not found on connector "${f}"`);let S=h.auth.provider,$=await s.get(S,d);if(null==$)throw Error(`No credential found for provider "${S}"`);let I=h.auth;if(null!=I.writeScopes&&!I.writeScopes.every(e=>$.scopes.includes(e))){let e=Error("INSUFFICIENT_SCOPES");throw e.code="INSUFFICIENT_SCOPES",e.requiredScopes=I.writeScopes,e}for(let e of Object.values(h.clusters)){for(let[r,o]of Object.entries(e.record))if(e.models[o].name===w){c=r;break}if(null!=c)break}if(null==c)throw Error(`Model "${w}" not found in connector "${f}" clusters`);let b=m;if("update"===p&&null!=E){let t=new TextEncoder().encode(`${f}:${E}`),n=e.create(r.fromString(c),d,t),l=await o(a),i=await l.getDocument(n);i?.data!=null&&(b={...i.data,...m})}let C=h.actionHandler({credential:$});if("create"===p)i=await C.create(w,b);else if("update"===p){if(null==E)throw Error("sourceID is required for update actions");i=await C.update(w,E,b)}else throw Error(`Unsupported action: ${p}`);let g={};for(let e of Object.values(h.clusters))if(null!=e.record[c]){g=e.models[e.record[c]]?.fieldsMeta??{};break}let k=new t({stores:a,modelID:c,ownerDID:d,connectorName:f,edgeFields:g,clusters:h.clusters}),v=await k.processBatch({entities:[i]});return{entity:i,documentID:v.documentIDs[0]??null}}
1
+ import { DocumentID, DocumentModelID } from '@kubun/id';
2
+ import { getGraphStore } from '@kubun/store-graph';
3
+ import { EntityProcessor } from './sync/processor.js';
4
+ export async function executeAction(params, deps) {
5
+ const { stores, registry, credentialProvider, ownerDID, logger, writeDocument } = deps;
6
+ const { connector: connectorName, action: actionName, model: modelName, input, sourceID } = params;
7
+ const connector = registry.get(connectorName);
8
+ if (connector == null) {
9
+ throw new Error(`Connector "${connectorName}" not found`);
10
+ }
11
+ if (connector.actionHandler == null) {
12
+ throw new Error(`Connector "${connectorName}" does not support actions`);
13
+ }
14
+ const action = connector.actions?.find((a)=>a.name === actionName);
15
+ if (action == null) {
16
+ throw new Error(`Action "${actionName}" not found on connector "${connectorName}"`);
17
+ }
18
+ // Fetch credential
19
+ const providerName = connector.auth.provider;
20
+ const credential = await credentialProvider.get(providerName, ownerDID);
21
+ if (credential == null) {
22
+ throw new Error(`No credential found for provider "${providerName}"`);
23
+ }
24
+ // Check write scopes
25
+ const auth = connector.auth;
26
+ if (auth.writeScopes != null) {
27
+ const hasWriteScopes = auth.writeScopes.every((scope)=>credential.scopes.includes(scope));
28
+ if (!hasWriteScopes) {
29
+ const error = new Error('INSUFFICIENT_SCOPES');
30
+ error.code = 'INSUFFICIENT_SCOPES';
31
+ error.requiredScopes = auth.writeScopes;
32
+ throw error;
33
+ }
34
+ }
35
+ // Resolve model ID from clusters
36
+ let modelID;
37
+ for (const cluster of Object.values(connector.clusters)){
38
+ for (const [id, index] of Object.entries(cluster.record)){
39
+ const model = cluster.models[index];
40
+ if (model.name === modelName) {
41
+ modelID = id;
42
+ break;
43
+ }
44
+ }
45
+ if (modelID != null) break;
46
+ }
47
+ if (modelID == null) {
48
+ throw new Error(`Model "${modelName}" not found in connector "${connectorName}" clusters`);
49
+ }
50
+ // For updates, merge contextual fields from existing document (e.g., calendarID)
51
+ let mergedInput = input;
52
+ if (actionName === 'update' && sourceID != null) {
53
+ const uniqueBytes = new TextEncoder().encode(`${connectorName}:${sourceID}`);
54
+ const docID = DocumentID.create(DocumentModelID.fromString(modelID), ownerDID, uniqueBytes);
55
+ const graphStore = await getGraphStore(stores);
56
+ const existingDoc = await graphStore.getDocument(docID);
57
+ if (existingDoc?.data != null) {
58
+ mergedInput = {
59
+ ...existingDoc.data,
60
+ ...input
61
+ };
62
+ }
63
+ }
64
+ // Dispatch to provider's action handler
65
+ const handler = connector.actionHandler({
66
+ credential
67
+ });
68
+ let entity;
69
+ if (actionName === 'create') {
70
+ entity = await handler.create(modelName, mergedInput);
71
+ } else if (actionName === 'update') {
72
+ if (sourceID == null) {
73
+ throw new Error('sourceID is required for update actions');
74
+ }
75
+ entity = await handler.update(modelName, sourceID, mergedInput);
76
+ } else {
77
+ throw new Error(`Unsupported action: ${actionName}`);
78
+ }
79
+ // Process entity into Kubun document via EntityProcessor
80
+ let edgeFields = {};
81
+ for (const cluster of Object.values(connector.clusters)){
82
+ if (cluster.record[modelID] != null) {
83
+ edgeFields = cluster.models[cluster.record[modelID]]?.fieldsMeta ?? {};
84
+ break;
85
+ }
86
+ }
87
+ const processor = new EntityProcessor({
88
+ stores,
89
+ modelID,
90
+ ownerDID,
91
+ connectorName,
92
+ logger,
93
+ edgeFields,
94
+ clusters: connector.clusters,
95
+ writeDocument
96
+ });
97
+ const result = await processor.processBatch({
98
+ entities: [
99
+ entity
100
+ ]
101
+ });
102
+ return {
103
+ entity,
104
+ documentID: result.documentIDs[0] ?? null
105
+ };
106
+ }
package/lib/api.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { StoreProvider } from '@kubun/db';
2
+ import type { Cipher } from '@kubun/engine';
3
+ import { type PendingAuthRecord } from '@kubun/store-connector';
2
4
  export type SyncStateData = {
3
5
  connectorName: string;
4
6
  ownerDID: string;
@@ -7,6 +9,7 @@ export type SyncStateData = {
7
9
  entityCount: number;
8
10
  status: string;
9
11
  error: string | null;
12
+ leaseExpiresAt: number | null;
10
13
  };
11
14
  export type SetSyncStateParams = {
12
15
  checkpoint?: string | null;
@@ -20,10 +23,16 @@ export type ConnectorAPI = {
20
23
  setSyncState: (connectorName: string, ownerDID: string, state: SetSyncStateParams) => Promise<SyncStateData>;
21
24
  deleteSyncState: (connectorName: string, ownerDID: string) => Promise<void>;
22
25
  listSyncStates: (connectorName: string) => Promise<Array<SyncStateData>>;
26
+ claimSync: (connectorName: string, ownerDID: string, leaseMs: number) => Promise<boolean>;
27
+ renewSync: (connectorName: string, ownerDID: string, leaseMs: number) => Promise<void>;
23
28
  getCredential: (providerName: string, ownerDID: string) => Promise<string | null>;
24
29
  setCredential: (providerName: string, ownerDID: string, credential: string) => Promise<void>;
25
30
  deleteCredential: (providerName: string, ownerDID: string) => Promise<void>;
31
+ createPendingAuth: (record: PendingAuthRecord) => Promise<void>;
32
+ consumePendingAuth: (state: string) => Promise<PendingAuthRecord | null>;
33
+ deleteExpiredPendingAuth: (cutoffMs: number) => Promise<void>;
26
34
  };
27
35
  export declare function createConnectorAPI(params: {
28
36
  stores: StoreProvider;
37
+ cipher: Cipher | undefined;
29
38
  }): ConnectorAPI;
package/lib/api.js CHANGED
@@ -1 +1,108 @@
1
- import{getConnectorStore as t}from"@kubun/store-connector";export function createConnectorAPI(e){return{async getSyncState(n,a){let r=await t(e.stores),s=await r.getSyncState(n,a);return null==s?null:{connectorName:n,ownerDID:a,checkpoint:null!=s.checkpoint?JSON.stringify(s.checkpoint):null,lastSyncedAt:s.lastSyncedAt,entityCount:s.entityCount,status:s.status,error:s.error??null}},async setSyncState(n,a,r){let s=await t(e.stores),c=r.lastSyncedAt??new Date().toISOString(),o=r.entityCount??0,l=r.checkpoint??null;await s.setSyncState(n,a,{checkpoint:null!=l?JSON.parse(l):null,lastSyncedAt:c,entityCount:o,status:r.status,error:r.error??void 0});let i=await s.getSyncState(n,a);if(null==i)throw Error("Failed to set sync state");return{connectorName:n,ownerDID:a,checkpoint:null!=i.checkpoint?JSON.stringify(i.checkpoint):null,lastSyncedAt:i.lastSyncedAt,entityCount:i.entityCount,status:i.status,error:i.error??null}},async deleteSyncState(n,a){let r=await t(e.stores);await r.deleteSyncState(n,a)},async listSyncStates(n){let a=await t(e.stores);return(await a.listSyncStatesForConnector(n)).map(t=>({connectorName:t.connectorName,ownerDID:t.ownerDID,checkpoint:null!=t.checkpoint?JSON.stringify(t.checkpoint):null,lastSyncedAt:t.lastSyncedAt,entityCount:t.entityCount,status:t.status,error:t.error??null}))},async getCredential(n,a){let r=await t(e.stores),s=await r.getCredential(n,a);return null==s?null:"string"==typeof s?s:JSON.stringify({accessToken:s.accessToken,refreshToken:s.refreshToken,expiresAt:s.expiresAt?.toISOString(),scopes:s.scopes,accountLabel:s.accountLabel,metadata:s.metadata})},async setCredential(n,a,r){let s=await t(e.stores);await s.setCredential(n,a,r)},async deleteCredential(n,a){let r=await t(e.stores);await r.deleteCredential(n,a)}}}
1
+ import { getConnectorStore } from '@kubun/store-connector';
2
+ // ---- Factory ----
3
+ export function createConnectorAPI(params) {
4
+ const getStore = ()=>getConnectorStore(params.stores);
5
+ const cipher = params.cipher;
6
+ return {
7
+ async getSyncState (connectorName, ownerDID) {
8
+ const store = await getStore();
9
+ const state = await store.getSyncState(connectorName, ownerDID);
10
+ if (state == null) return null;
11
+ return {
12
+ connectorName,
13
+ ownerDID,
14
+ checkpoint: state.checkpoint != null ? JSON.stringify(state.checkpoint) : null,
15
+ lastSyncedAt: state.lastSyncedAt,
16
+ entityCount: state.entityCount,
17
+ status: state.status,
18
+ error: state.error ?? null,
19
+ leaseExpiresAt: state.leaseExpiresAt ?? null
20
+ };
21
+ },
22
+ async setSyncState (connectorName, ownerDID, state) {
23
+ const store = await getStore();
24
+ const lastSyncedAt = state.lastSyncedAt ?? new Date().toISOString();
25
+ const entityCount = state.entityCount ?? 0;
26
+ const checkpoint = state.checkpoint ?? null;
27
+ await store.setSyncState(connectorName, ownerDID, {
28
+ checkpoint: checkpoint != null ? JSON.parse(checkpoint) : null,
29
+ lastSyncedAt,
30
+ entityCount,
31
+ status: state.status,
32
+ error: state.error ?? undefined
33
+ });
34
+ const saved = await store.getSyncState(connectorName, ownerDID);
35
+ if (saved == null) {
36
+ throw new Error('Failed to set sync state');
37
+ }
38
+ return {
39
+ connectorName,
40
+ ownerDID,
41
+ checkpoint: saved.checkpoint != null ? JSON.stringify(saved.checkpoint) : null,
42
+ lastSyncedAt: saved.lastSyncedAt,
43
+ entityCount: saved.entityCount,
44
+ status: saved.status,
45
+ error: saved.error ?? null,
46
+ leaseExpiresAt: saved.leaseExpiresAt ?? null
47
+ };
48
+ },
49
+ async deleteSyncState (connectorName, ownerDID) {
50
+ const store = await getStore();
51
+ await store.deleteSyncState(connectorName, ownerDID);
52
+ },
53
+ async listSyncStates (connectorName) {
54
+ const store = await getStore();
55
+ const entries = await store.listSyncStatesForConnector(connectorName);
56
+ return entries.map((entry)=>({
57
+ connectorName: entry.connectorName,
58
+ ownerDID: entry.ownerDID,
59
+ checkpoint: entry.checkpoint != null ? JSON.stringify(entry.checkpoint) : null,
60
+ lastSyncedAt: entry.lastSyncedAt,
61
+ entityCount: entry.entityCount,
62
+ status: entry.status,
63
+ error: entry.error ?? null,
64
+ leaseExpiresAt: entry.leaseExpiresAt ?? null
65
+ }));
66
+ },
67
+ async claimSync (connectorName, ownerDID, leaseMs) {
68
+ const store = await getStore();
69
+ return store.claimSync(connectorName, ownerDID, leaseMs);
70
+ },
71
+ async renewSync (connectorName, ownerDID, leaseMs) {
72
+ const store = await getStore();
73
+ await store.renewSync(connectorName, ownerDID, leaseMs);
74
+ },
75
+ async getCredential (providerName, ownerDID) {
76
+ const store = await getStore();
77
+ const stored = await store.getCredential(providerName, ownerDID);
78
+ if (stored == null) return null;
79
+ if (cipher == null) {
80
+ throw new Error('Cannot decrypt connector credential: no at-rest cipher available');
81
+ }
82
+ return cipher.decrypt(stored);
83
+ },
84
+ async setCredential (providerName, ownerDID, credential) {
85
+ if (cipher == null) {
86
+ throw new Error('Cannot persist connector credential: no at-rest cipher available');
87
+ }
88
+ const store = await getStore();
89
+ await store.setCredential(providerName, ownerDID, cipher.encrypt(credential));
90
+ },
91
+ async deleteCredential (providerName, ownerDID) {
92
+ const store = await getStore();
93
+ await store.deleteCredential(providerName, ownerDID);
94
+ },
95
+ async createPendingAuth (record) {
96
+ const store = await getStore();
97
+ await store.createPendingAuth(record);
98
+ },
99
+ async consumePendingAuth (state) {
100
+ const store = await getStore();
101
+ return store.consumePendingAuth(state);
102
+ },
103
+ async deleteExpiredPendingAuth (cutoffMs) {
104
+ const store = await getStore();
105
+ await store.deleteExpiredPendingAuth(cutoffMs);
106
+ }
107
+ };
108
+ }
package/lib/boundary.js CHANGED
@@ -1 +1,73 @@
1
- let e=/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;export function parseDuration(n){let i=e.exec(n);if(!i)return;let t=Number.parseInt(i[1]??"0",10),l=Number.parseInt(i[2]??"0",10),a=Number.parseInt(i[3]??"0",10),u=Number.parseInt(i[4]??"0",10);if(0!==t||0!==l||0!==a||0!==u)return((24*t+l)*60+a)*6e4+1e3*u}export function computeEffectiveBoundary(e,n){if(null==n)return e;let i={};if(null!=e.maxAge||null!=n.maxAge){let t=e.maxAge?parseDuration(e.maxAge):void 0,l=n.maxAge?parseDuration(n.maxAge):void 0;null!=t&&null!=l?i.maxAge=t<=l?e.maxAge:n.maxAge:i.maxAge=e.maxAge??n.maxAge}if(null!=e.since||null!=n.since){let t=e.since?new Date(e.since):void 0,l=n.since?new Date(n.since):void 0;null!=t&&null!=l?i.since=t>=l?e.since:n.since:i.since=e.since??n.since}if(null!=e.maxEntities||null!=n.maxEntities){let t=e.maxEntities,l=n.maxEntities;null!=t&&null!=l?i.maxEntities=Math.min(t,l):i.maxEntities=e.maxEntities??n.maxEntities}return i}export function isWithinBoundary(e,n){let i=Date.now();if(null!=n.maxAge){let t=parseDuration(n.maxAge);if(null!=t&&e.getTime()<i-t)return!1}if(null!=n.since){let i=new Date(n.since);if(e.getTime()<i.getTime())return!1}return!0}
1
+ const DURATION_REGEX = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
2
+ export function parseDuration(duration) {
3
+ const match = DURATION_REGEX.exec(duration);
4
+ if (!match) {
5
+ return undefined;
6
+ }
7
+ const days = Number.parseInt(match[1] ?? '0', 10);
8
+ const hours = Number.parseInt(match[2] ?? '0', 10);
9
+ const minutes = Number.parseInt(match[3] ?? '0', 10);
10
+ const seconds = Number.parseInt(match[4] ?? '0', 10);
11
+ if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) {
12
+ return undefined;
13
+ }
14
+ return ((days * 24 + hours) * 60 + minutes) * 60 * 1000 + seconds * 1000;
15
+ }
16
+ export function computeEffectiveBoundary(serverBoundary, userBoundary) {
17
+ if (userBoundary == null) {
18
+ return serverBoundary;
19
+ }
20
+ const result = {};
21
+ // maxAge: take the shorter duration (more restrictive)
22
+ if (serverBoundary.maxAge != null || userBoundary.maxAge != null) {
23
+ const serverMs = serverBoundary.maxAge ? parseDuration(serverBoundary.maxAge) : undefined;
24
+ const userMs = userBoundary.maxAge ? parseDuration(userBoundary.maxAge) : undefined;
25
+ if (serverMs != null && userMs != null) {
26
+ result.maxAge = serverMs <= userMs ? serverBoundary.maxAge : userBoundary.maxAge;
27
+ } else {
28
+ result.maxAge = serverBoundary.maxAge ?? userBoundary.maxAge;
29
+ }
30
+ }
31
+ // since: take the more recent date (more restrictive)
32
+ if (serverBoundary.since != null || userBoundary.since != null) {
33
+ const serverDate = serverBoundary.since ? new Date(serverBoundary.since) : undefined;
34
+ const userDate = userBoundary.since ? new Date(userBoundary.since) : undefined;
35
+ if (serverDate != null && userDate != null) {
36
+ result.since = serverDate >= userDate ? serverBoundary.since : userBoundary.since;
37
+ } else {
38
+ result.since = serverBoundary.since ?? userBoundary.since;
39
+ }
40
+ }
41
+ // maxEntities: take the smaller count (more restrictive)
42
+ if (serverBoundary.maxEntities != null || userBoundary.maxEntities != null) {
43
+ const serverCount = serverBoundary.maxEntities;
44
+ const userCount = userBoundary.maxEntities;
45
+ if (serverCount != null && userCount != null) {
46
+ result.maxEntities = Math.min(serverCount, userCount);
47
+ } else {
48
+ result.maxEntities = serverBoundary.maxEntities ?? userBoundary.maxEntities;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ export function isWithinBoundary(date, boundary) {
54
+ const now = Date.now();
55
+ // Check maxAge constraint
56
+ if (boundary.maxAge != null) {
57
+ const maxAgeMs = parseDuration(boundary.maxAge);
58
+ if (maxAgeMs != null) {
59
+ const cutoff = now - maxAgeMs;
60
+ if (date.getTime() < cutoff) {
61
+ return false;
62
+ }
63
+ }
64
+ }
65
+ // Check since constraint
66
+ if (boundary.since != null) {
67
+ const sinceDate = new Date(boundary.since);
68
+ if (date.getTime() < sinceDate.getTime()) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
@@ -1,5 +1,5 @@
1
- import type { Runtime } from '@enkaku/runtime';
2
1
  import type { Credential, CredentialProvider, OAuthProviderDefinition } from '@kubun/connector';
2
+ import type { Runtime } from '@sozai/runtime';
3
3
  import type { ConnectorAPI } from './api.js';
4
4
  export type DBCredentialProviderParams = {
5
5
  api: ConnectorAPI;
package/lib/credential.js CHANGED
@@ -1 +1,107 @@
1
- export class DBCredentialProvider{#e;#t;#n;#r;constructor(e){this.#e=e.api,this.#t=e.runtime,this.#n=e.providers,this.#r=(e.bufferSeconds??300)*1e3}async get(e,t){let n,r,s=await this.#e.getCredential(e,t);if(null==s)return null;let a=(r={accessToken:(n="string"==typeof s?JSON.parse(s):s).accessToken,scopes:n.scopes},null!=n.refreshToken&&(r.refreshToken=n.refreshToken),null!=n.expiresAt&&(r.expiresAt=new Date(n.expiresAt)),null!=n.accountLabel&&(r.accountLabel=n.accountLabel),null!=n.metadata&&(r.metadata=n.metadata),r);return this.#s(a)&&null!=a.refreshToken?await this.#a(e,t,a,a.refreshToken)??a:a}async set(e,t,n){let r;await this.#e.setCredential(e,t,(r={accessToken:n.accessToken,scopes:n.scopes},null!=n.refreshToken&&(r.refreshToken=n.refreshToken),null!=n.expiresAt&&(r.expiresAt=n.expiresAt.toISOString()),null!=n.accountLabel&&(r.accountLabel=n.accountLabel),null!=n.metadata&&(r.metadata=n.metadata),JSON.stringify(r)))}async delete(e,t){await this.#e.deleteCredential(e,t)}#s(e){return null!=e.expiresAt&&e.expiresAt.getTime()-Date.now()<this.#r}async #a(e,t,n,r){let s=this.#n.find(t=>t.name===e);if(null==s||null==s.clientID||null==s.clientSecret)return null;try{let a=await this.#t.fetch(s.tokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:r,client_id:s.clientID,client_secret:s.clientSecret})});if(!a.ok)return null;let i=await a.json(),l={accessToken:i.access_token,refreshToken:i.refresh_token??n.refreshToken,expiresAt:null!=i.expires_in?new Date(Date.now()+1e3*i.expires_in):void 0,scopes:i.scope?.split(" ")??n.scopes,accountLabel:n.accountLabel,metadata:n.metadata};return await this.set(e,t,l),l}catch{return null}}}
1
+ const DEFAULT_BUFFER_SECONDS = 300;
2
+ function serializeCredential(credential) {
3
+ const serialized = {
4
+ accessToken: credential.accessToken,
5
+ scopes: credential.scopes
6
+ };
7
+ if (credential.refreshToken != null) {
8
+ serialized.refreshToken = credential.refreshToken;
9
+ }
10
+ if (credential.expiresAt != null) {
11
+ serialized.expiresAt = credential.expiresAt.toISOString();
12
+ }
13
+ if (credential.accountLabel != null) {
14
+ serialized.accountLabel = credential.accountLabel;
15
+ }
16
+ if (credential.metadata != null) {
17
+ serialized.metadata = credential.metadata;
18
+ }
19
+ return JSON.stringify(serialized);
20
+ }
21
+ function deserializeCredential(raw) {
22
+ // ParseJSONResultsPlugin may have already parsed the JSON string
23
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
24
+ const credential = {
25
+ accessToken: parsed.accessToken,
26
+ scopes: parsed.scopes
27
+ };
28
+ if (parsed.refreshToken != null) {
29
+ credential.refreshToken = parsed.refreshToken;
30
+ }
31
+ if (parsed.expiresAt != null) {
32
+ credential.expiresAt = new Date(parsed.expiresAt);
33
+ }
34
+ if (parsed.accountLabel != null) {
35
+ credential.accountLabel = parsed.accountLabel;
36
+ }
37
+ if (parsed.metadata != null) {
38
+ credential.metadata = parsed.metadata;
39
+ }
40
+ return credential;
41
+ }
42
+ export class DBCredentialProvider {
43
+ #api;
44
+ #runtime;
45
+ #providers;
46
+ #bufferMs;
47
+ constructor(params){
48
+ this.#api = params.api;
49
+ this.#runtime = params.runtime;
50
+ this.#providers = params.providers;
51
+ this.#bufferMs = (params.bufferSeconds ?? DEFAULT_BUFFER_SECONDS) * 1000;
52
+ }
53
+ async get(providerName, ownerDID) {
54
+ const raw = await this.#api.getCredential(providerName, ownerDID);
55
+ if (raw == null) return null;
56
+ const credential = deserializeCredential(raw);
57
+ if (this.#isExpiringSoon(credential) && credential.refreshToken != null) {
58
+ const refreshed = await this.#refresh(providerName, ownerDID, credential, credential.refreshToken);
59
+ return refreshed ?? credential;
60
+ }
61
+ return credential;
62
+ }
63
+ async set(providerName, ownerDID, credential) {
64
+ await this.#api.setCredential(providerName, ownerDID, serializeCredential(credential));
65
+ }
66
+ async delete(providerName, ownerDID) {
67
+ await this.#api.deleteCredential(providerName, ownerDID);
68
+ }
69
+ #isExpiringSoon(credential) {
70
+ if (credential.expiresAt == null) return false;
71
+ return credential.expiresAt.getTime() - Date.now() < this.#bufferMs;
72
+ }
73
+ async #refresh(providerName, ownerDID, credential, refreshToken) {
74
+ const provider = this.#providers.find((p)=>p.name === providerName);
75
+ if (provider == null || provider.clientID == null || provider.clientSecret == null) {
76
+ return null;
77
+ }
78
+ try {
79
+ const response = await this.#runtime.fetch(provider.tokenEndpoint, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Content-Type': 'application/x-www-form-urlencoded'
83
+ },
84
+ body: new URLSearchParams({
85
+ grant_type: 'refresh_token',
86
+ refresh_token: refreshToken,
87
+ client_id: provider.clientID,
88
+ client_secret: provider.clientSecret
89
+ })
90
+ });
91
+ if (!response.ok) return null;
92
+ const tokenData = await response.json();
93
+ const refreshed = {
94
+ accessToken: tokenData.access_token,
95
+ refreshToken: tokenData.refresh_token ?? credential.refreshToken,
96
+ expiresAt: tokenData.expires_in != null ? new Date(Date.now() + tokenData.expires_in * 1000) : undefined,
97
+ scopes: tokenData.scope?.split(' ') ?? credential.scopes,
98
+ accountLabel: credential.accountLabel,
99
+ metadata: credential.metadata
100
+ };
101
+ await this.set(providerName, ownerDID, refreshed);
102
+ return refreshed;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ }
package/lib/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export type { CompleteConnectorAuthInput, CompleteConnectorAuthOutput, Connector
10
10
  export { createConnectorSchemaExtension, toConnectorSyncEventResult } from './schema.js';
11
11
  export { type RunSyncParams, SyncEngine, type SyncEngineParams } from './sync/engine.js';
12
12
  export { type OrchestrateSyncParams, type OrchestrateSyncResult, orchestrateSync, type SyncEventEmitter, } from './sync/orchestrate.js';
13
- export { EntityProcessor, type EntityProcessorParams, type ProcessBatchResult, } from './sync/processor.js';
13
+ export { EntityProcessor, type EntityProcessorParams, type MutateDocuments, type ProcessBatchResult, type SignedDocumentWriter, } from './sync/processor.js';
14
14
  export { DBSyncStateStore } from './sync/state.js';
15
15
  export type { ConnectorAPI, SetSyncStateParams, SyncStateData };
16
16
  export { DBCredentialProvider, type DBCredentialProviderParams };
@@ -21,5 +21,7 @@ export type ConnectorPluginOptions = {
21
21
  boundary?: SyncBoundary;
22
22
  pollingInterval?: string;
23
23
  };
24
+ /** Override the sync-lease TTL in milliseconds (defaults to 5 minutes). */
25
+ syncLeaseTTL?: number;
24
26
  };
25
27
  export declare function createConnectorPlugin(options: ConnectorPluginOptions): (params: PluginFactoryParams) => KubunPlugin;