@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 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
- getCredential(providerName: string, ownerDID: string): Promise<string | null>;
26
- setCredential(providerName: string, ownerDID: string, credential: string): Promise<void>;
27
- deleteCredential(providerName: string, ownerDID: string): Promise<void>;
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 getCredential (providerName, ownerDID) {
25
- const row = await db.selectFrom('kubun_connector_credentials').select('credential').where('provider_name', '=', providerName).where('owner_did', '=', ownerDID).executeTakeFirst();
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.credential;
27
+ return row.entry_id;
28
28
  },
29
- async setCredential (providerName, ownerDID, credential) {
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
- credential
33
+ entry_id: entryID
34
34
  }).onConflict((oc)=>oc.columns([
35
35
  'provider_name',
36
36
  'owner_did'
37
37
  ]).doUpdateSet({
38
- credential
38
+ entry_id: entryID
39
39
  })).execute();
40
40
  },
41
- async deleteCredential (providerName, ownerDID) {
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
- // Progress write only: the lease column is owned by claimSync/renewSync,
52
- // never by a plain set. A fresh INSERT carries no lease (null); on
53
- // conflict the lease is deliberately excluded so a live lease held by the
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('credential', t.text, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_connector_credentials_pkey', [
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())// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
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
- credential: string;
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.12.1",
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.12.1",
20
- "@kubun/db-adapter": "^0.13.0"
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.12.1",
25
- "@kubun/db-postgres": "^0.12.1"
24
+ "@kubun/db-better-sqlite": "^0.13.0",
25
+ "@kubun/db-postgres": "^0.13.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"