@kubun/store-connector 0.12.1 → 0.13.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/api.d.ts +7 -5
- package/lib/api.js +14 -50
- package/lib/migrations.js +6 -2
- package/lib/tables.d.ts +17 -1
- package/package.json +5 -5
package/lib/api.d.ts
CHANGED
|
@@ -19,18 +19,20 @@ export type PendingAuthRecord = {
|
|
|
19
19
|
codeVerifier: string;
|
|
20
20
|
provider: string;
|
|
21
21
|
ownerDID: string;
|
|
22
|
+
/** The owner's DID in a form that can be encrypted to. See the table type. */
|
|
23
|
+
ownerWrappableDID?: string | null;
|
|
24
|
+
/** A recovery recipient's DID in a form that can be encrypted to. See the table type. */
|
|
25
|
+
controllerWrappableDID?: string | null;
|
|
22
26
|
createdAt: number;
|
|
23
27
|
};
|
|
24
28
|
export type ConnectorStoreAPI = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
getCredentialEntryID(providerName: string, ownerDID: string): Promise<string | null>;
|
|
30
|
+
setCredentialEntryID(providerName: string, ownerDID: string, entryID: string): Promise<void>;
|
|
31
|
+
deleteCredentialEntryID(providerName: string, ownerDID: string): Promise<void>;
|
|
28
32
|
getSyncState(connectorName: string, ownerDID: string): Promise<SyncState | null>;
|
|
29
33
|
setSyncState(connectorName: string, ownerDID: string, state: SyncState): Promise<void>;
|
|
30
34
|
deleteSyncState(connectorName: string, ownerDID: string): Promise<void>;
|
|
31
35
|
listSyncStatesForConnector(connectorName: string): Promise<Array<SyncStateEntry>>;
|
|
32
|
-
claimSync(connectorName: string, ownerDID: string, leaseMs: number): Promise<boolean>;
|
|
33
|
-
renewSync(connectorName: string, ownerDID: string, leaseMs: number): Promise<void>;
|
|
34
36
|
createPendingAuth(record: PendingAuthRecord): Promise<void>;
|
|
35
37
|
consumePendingAuth(state: string): Promise<PendingAuthRecord | null>;
|
|
36
38
|
deleteExpiredPendingAuth(cutoffMs: number): Promise<void>;
|
package/lib/api.js
CHANGED
|
@@ -21,24 +21,24 @@ function rowToSyncState(row) {
|
|
|
21
21
|
export function createConnectorStore(db, _adapter) {
|
|
22
22
|
return {
|
|
23
23
|
// --- Credential operations ---
|
|
24
|
-
async
|
|
25
|
-
const row = await db.selectFrom('kubun_connector_credentials').select('
|
|
24
|
+
async getCredentialEntryID (providerName, ownerDID) {
|
|
25
|
+
const row = await db.selectFrom('kubun_connector_credentials').select('entry_id').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).executeTakeFirst();
|
|
26
26
|
if (row == null) return null;
|
|
27
|
-
return row.
|
|
27
|
+
return row.entry_id;
|
|
28
28
|
},
|
|
29
|
-
async
|
|
29
|
+
async setCredentialEntryID (providerName, ownerDID, entryID) {
|
|
30
30
|
await db.insertInto('kubun_connector_credentials').values({
|
|
31
31
|
provider_name: providerName,
|
|
32
32
|
owner_did: ownerDID,
|
|
33
|
-
|
|
33
|
+
entry_id: entryID
|
|
34
34
|
}).onConflict((oc)=>oc.columns([
|
|
35
35
|
'provider_name',
|
|
36
36
|
'owner_did'
|
|
37
37
|
]).doUpdateSet({
|
|
38
|
-
|
|
38
|
+
entry_id: entryID
|
|
39
39
|
})).execute();
|
|
40
40
|
},
|
|
41
|
-
async
|
|
41
|
+
async deleteCredentialEntryID (providerName, ownerDID) {
|
|
42
42
|
await db.deleteFrom('kubun_connector_credentials').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).execute();
|
|
43
43
|
},
|
|
44
44
|
// --- Sync state operations ---
|
|
@@ -48,10 +48,9 @@ export function createConnectorStore(db, _adapter) {
|
|
|
48
48
|
return rowToSyncState(row);
|
|
49
49
|
},
|
|
50
50
|
async setSyncState (connectorName, ownerDID, state) {
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// sync worker survives progress updates.
|
|
51
|
+
// The `lease_expires_at` column is dead (the lease moved to the workflow
|
|
52
|
+
// engine); a fresh INSERT sets it null and a conflicting update leaves it
|
|
53
|
+
// untouched.
|
|
55
54
|
await db.insertInto('kubun_connector_sync_state').values({
|
|
56
55
|
connector_name: connectorName,
|
|
57
56
|
owner_did: ownerDID,
|
|
@@ -72,45 +71,6 @@ export function createConnectorStore(db, _adapter) {
|
|
|
72
71
|
error: state.error ?? null
|
|
73
72
|
})).execute();
|
|
74
73
|
},
|
|
75
|
-
async claimSync (connectorName, ownerDID, leaseMs) {
|
|
76
|
-
const now = Date.now();
|
|
77
|
-
const leaseExpiresAt = now + leaseMs;
|
|
78
|
-
// Single atomic upsert. INSERT wins when no row exists. On conflict the
|
|
79
|
-
// DO UPDATE only fires when its WHERE holds: the existing lease is not
|
|
80
|
-
// 'syncing', is unset, or has expired. The guard columns are
|
|
81
|
-
// table-qualified because a bare 'status'/'lease_expires_at' is ambiguous
|
|
82
|
-
// on Postgres (both the target row and the excluded row are in scope).
|
|
83
|
-
// A live lease fails the WHERE, so no row is returned.
|
|
84
|
-
const result = await db.insertInto('kubun_connector_sync_state').values({
|
|
85
|
-
connector_name: connectorName,
|
|
86
|
-
owner_did: ownerDID,
|
|
87
|
-
checkpoint: null,
|
|
88
|
-
last_synced_at: new Date(now).toISOString(),
|
|
89
|
-
entity_count: 0,
|
|
90
|
-
status: 'syncing',
|
|
91
|
-
error: null,
|
|
92
|
-
lease_expires_at: leaseExpiresAt
|
|
93
|
-
}).onConflict((oc)=>oc.columns([
|
|
94
|
-
'connector_name',
|
|
95
|
-
'owner_did'
|
|
96
|
-
]).doUpdateSet({
|
|
97
|
-
status: 'syncing',
|
|
98
|
-
lease_expires_at: leaseExpiresAt
|
|
99
|
-
}).where((eb)=>eb.or([
|
|
100
|
-
eb('kubun_connector_sync_state.status', '!=', 'syncing'),
|
|
101
|
-
eb('kubun_connector_sync_state.lease_expires_at', 'is', null),
|
|
102
|
-
eb('kubun_connector_sync_state.lease_expires_at', '<', now)
|
|
103
|
-
]))).returningAll().executeTakeFirst();
|
|
104
|
-
return result != null;
|
|
105
|
-
},
|
|
106
|
-
async renewSync (connectorName, ownerDID, leaseMs) {
|
|
107
|
-
const now = Date.now();
|
|
108
|
-
// Extend the lease only while the row is still actively syncing. A plain
|
|
109
|
-
// UPDATE (no excluded row in scope) needs no table-qualified columns.
|
|
110
|
-
await db.updateTable('kubun_connector_sync_state').set({
|
|
111
|
-
lease_expires_at: now + leaseMs
|
|
112
|
-
}).where('connector_name', '=', connectorName).where('owner_did', '=', ownerDID).where('status', '=', 'syncing').execute();
|
|
113
|
-
},
|
|
114
74
|
async deleteSyncState (connectorName, ownerDID) {
|
|
115
75
|
await db.deleteFrom('kubun_connector_sync_state').where('connector_name', '=', connectorName).where('owner_did', '=', ownerDID).execute();
|
|
116
76
|
},
|
|
@@ -129,6 +89,8 @@ export function createConnectorStore(db, _adapter) {
|
|
|
129
89
|
code_verifier: record.codeVerifier,
|
|
130
90
|
provider: record.provider,
|
|
131
91
|
owner_did: record.ownerDID,
|
|
92
|
+
owner_wrappable_did: record.ownerWrappableDID ?? null,
|
|
93
|
+
controller_wrappable_did: record.controllerWrappableDID ?? null,
|
|
132
94
|
created_at: record.createdAt
|
|
133
95
|
}).execute();
|
|
134
96
|
},
|
|
@@ -142,6 +104,8 @@ export function createConnectorStore(db, _adapter) {
|
|
|
142
104
|
codeVerifier: row.code_verifier,
|
|
143
105
|
provider: row.provider,
|
|
144
106
|
ownerDID: row.owner_did,
|
|
107
|
+
ownerWrappableDID: row.owner_wrappable_did,
|
|
108
|
+
controllerWrappableDID: row.controller_wrappable_did,
|
|
145
109
|
createdAt: row.created_at
|
|
146
110
|
};
|
|
147
111
|
},
|
package/lib/migrations.js
CHANGED
|
@@ -12,13 +12,17 @@ export function getConnectorMigrations(ctx) {
|
|
|
12
12
|
'owner_did'
|
|
13
13
|
]).execute();
|
|
14
14
|
// Connector credentials
|
|
15
|
-
await db.schema.createTable('kubun_connector_credentials').ifNotExists().addColumn('provider_name', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('
|
|
15
|
+
await db.schema.createTable('kubun_connector_credentials').ifNotExists().addColumn('provider_name', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('entry_id', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_connector_credentials_pkey', [
|
|
16
16
|
'provider_name',
|
|
17
17
|
'owner_did'
|
|
18
18
|
]).execute();
|
|
19
19
|
// Pending OAuth authorizations: state + PKCE verifier persisted between the
|
|
20
20
|
// authorize redirect and the callback. State is single-use, consumed on read.
|
|
21
|
-
await db.schema.createTable('kubun_connector_pending_auth').ifNotExists().addColumn('state', t.text, (col)=>col.notNull().primaryKey()).addColumn('code_verifier', t.text, (col)=>col.notNull()).addColumn('provider', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull())//
|
|
21
|
+
await db.schema.createTable('kubun_connector_pending_auth').ifNotExists().addColumn('state', t.text, (col)=>col.notNull().primaryKey()).addColumn('code_verifier', t.text, (col)=>col.notNull()).addColumn('provider', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull())// The wrappable spelling of owner_did, kept beside it rather than
|
|
22
|
+
// replacing it: owner_did is the join key everywhere and is normalized.
|
|
23
|
+
.addColumn('owner_wrappable_did', t.text)// The wrappable DID of the user's controller, carried from authorize to
|
|
24
|
+
// callback so the mint can add the controller recovery wrap.
|
|
25
|
+
.addColumn('controller_wrappable_did', t.text)// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
22
26
|
// the adapter's int8 parser reads it back as a JS number.
|
|
23
27
|
.addColumn('created_at', t.bigint, (col)=>col.notNull()).execute();
|
|
24
28
|
},
|
package/lib/tables.d.ts
CHANGED
|
@@ -10,10 +10,14 @@ export type ConnectorSyncStateTable = {
|
|
|
10
10
|
lease_expires_at: number | null;
|
|
11
11
|
};
|
|
12
12
|
export type ConnectorSyncState = Selectable<ConnectorSyncStateTable>;
|
|
13
|
+
/**
|
|
14
|
+
* A pointer, not a secret. The token lives in a credential entry; this table
|
|
15
|
+
* only records which entry holds the credential for a `(provider, owner)` pair.
|
|
16
|
+
*/
|
|
13
17
|
export type ConnectorCredentialTable = {
|
|
14
18
|
provider_name: string;
|
|
15
19
|
owner_did: string;
|
|
16
|
-
|
|
20
|
+
entry_id: string;
|
|
17
21
|
};
|
|
18
22
|
export type ConnectorCredential = Selectable<ConnectorCredentialTable>;
|
|
19
23
|
export type ConnectorPendingAuthTable = {
|
|
@@ -21,6 +25,18 @@ export type ConnectorPendingAuthTable = {
|
|
|
21
25
|
code_verifier: string;
|
|
22
26
|
provider: string;
|
|
23
27
|
owner_did: string;
|
|
28
|
+
/**
|
|
29
|
+
* The owner's DID in a form that can be encrypted to, carried from the
|
|
30
|
+
* authorize call to the callback that mints the key. Null when the caller did
|
|
31
|
+
* not supply one, which leaves the credential creatable only if `owner_did`
|
|
32
|
+
* happens to be wrappable on its own.
|
|
33
|
+
*/
|
|
34
|
+
owner_wrappable_did: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* A recovery recipient's DID in a form that can be encrypted to, carried the
|
|
37
|
+
* same way as `owner_wrappable_did`. Null when the caller supplied none.
|
|
38
|
+
*/
|
|
39
|
+
controller_wrappable_did: string | null;
|
|
24
40
|
created_at: number;
|
|
25
41
|
};
|
|
26
42
|
export type ConnectorPendingAuth = Selectable<ConnectorPendingAuthTable>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"kysely": "^0.29.5",
|
|
19
|
-
"@kubun/db": "^0.
|
|
20
|
-
"@kubun/db
|
|
19
|
+
"@kubun/db-adapter": "^0.13.0",
|
|
20
|
+
"@kubun/db": "^0.13.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@testcontainers/postgresql": "^12.1.0",
|
|
24
|
-
"@kubun/db-better-sqlite": "^0.
|
|
25
|
-
"@kubun/db-postgres": "^0.
|
|
24
|
+
"@kubun/db-better-sqlite": "^0.13.0",
|
|
25
|
+
"@kubun/db-postgres": "^0.13.0"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|