@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 +10 -0
- package/lib/action.js +106 -1
- package/lib/api.d.ts +9 -0
- package/lib/api.js +108 -1
- package/lib/boundary.js +73 -1
- package/lib/credential.d.ts +1 -1
- package/lib/credential.js +107 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +223 -1
- package/lib/manager.d.ts +1 -1
- package/lib/manager.js +90 -1
- package/lib/oauth.d.ts +5 -3
- package/lib/oauth.js +137 -1
- package/lib/registry.js +24 -1
- package/lib/schema.d.ts +44 -2
- package/lib/schema.js +298 -11
- package/lib/sync/engine.d.ts +6 -0
- package/lib/sync/engine.js +163 -1
- package/lib/sync/orchestrate.d.ts +6 -0
- package/lib/sync/orchestrate.js +100 -1
- package/lib/sync/processor.d.ts +34 -2
- package/lib/sync/processor.js +191 -1
- package/lib/sync/state.d.ts +2 -0
- package/lib/sync/state.js +57 -1
- package/lib/write-grants.d.ts +35 -0
- package/lib/write-grants.js +286 -0
- package/package.json +25 -19
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/credential.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|