@kubun/credential 0.17.0 → 0.17.2
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 +33 -0
- package/lib/api.js +104 -8
- package/lib/errors.d.ts +50 -1
- package/lib/errors.js +80 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/manager.d.ts +14 -0
- package/lib/manager.js +45 -0
- package/lib/provider.d.ts +13 -2
- package/lib/provider.js +19 -0
- package/lib/refreshing-credential-provider.d.ts +5 -4
- package/lib/refreshing-credential-provider.js +32 -8
- package/lib/validate.d.ts +2 -0
- package/lib/validate.js +30 -0
- package/package.json +8 -4
package/lib/api.d.ts
CHANGED
|
@@ -37,6 +37,19 @@ export type CredentialAPI = {
|
|
|
37
37
|
credential: string;
|
|
38
38
|
}) => Promise<void>;
|
|
39
39
|
deleteCredential: (providerName: string, ownerDID: string) => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Point `(providerName, ownerDID)` at an entry this device already holds,
|
|
42
|
+
* without minting anything.
|
|
43
|
+
*
|
|
44
|
+
* Gate B first: the entry's key must be owned by `ownerDID` (normalized on
|
|
45
|
+
* both sides), because the vault can decrypt any entry it holds a server
|
|
46
|
+
* wrapping for — decryptability is not authority. An idempotent retry that
|
|
47
|
+
* already points at `entryID` returns before that entry is ever decrypted, so
|
|
48
|
+
* a safe retry cannot fail on a branch/version check inside the read. Only
|
|
49
|
+
* then is the decrypted plaintext checked against the `Credential` shape, so a
|
|
50
|
+
* malformed or mistyped entry can never become a resolvable alias.
|
|
51
|
+
*/
|
|
52
|
+
bindAlias: (providerName: string, ownerDID: string, entryID: string) => Promise<void>;
|
|
40
53
|
/**
|
|
41
54
|
* The normalized owner DIDs that currently hold a credential for `providerName`.
|
|
42
55
|
*
|
|
@@ -54,6 +67,26 @@ export type CredentialAPI = {
|
|
|
54
67
|
* agreement key is resolvable only through the controller resolver.
|
|
55
68
|
*/
|
|
56
69
|
getControllerMethods: () => MethodRegistry;
|
|
70
|
+
/**
|
|
71
|
+
* Like `getCredential`, but pairs the serialized credential with the entryID
|
|
72
|
+
* it resolved through the alias — so a caller can then target that entry
|
|
73
|
+
* directly (e.g. `setCredentialForEntry`) without re-resolving the alias.
|
|
74
|
+
*/
|
|
75
|
+
getCredentialRef: (providerName: string, ownerDID: string) => Promise<{
|
|
76
|
+
credential: string;
|
|
77
|
+
entryID: string;
|
|
78
|
+
} | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Write to a specific entry, not the alias — the counterpart to
|
|
81
|
+
* `getCredentialRef`. Presence-gated on the entry itself (via
|
|
82
|
+
* `manager.updateEntryIfPresent`), so it never resurrects a deleted entry.
|
|
83
|
+
*/
|
|
84
|
+
setCredentialForEntry: (params: {
|
|
85
|
+
providerName: string;
|
|
86
|
+
ownerDID: string;
|
|
87
|
+
entryID: string;
|
|
88
|
+
credential: string;
|
|
89
|
+
}) => Promise<'updated' | 'absent'>;
|
|
57
90
|
};
|
|
58
91
|
export type CreateCredentialAPIParams = {
|
|
59
92
|
stores: StoreProvider;
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { getCredentialStore } from '@kubun/store-credential';
|
|
3
3
|
import { fromUTF, toUTF } from '@sozai/codec';
|
|
4
|
-
import { CredentialSupersededBranch } from './errors.js';
|
|
4
|
+
import { CredentialAliasAlreadyBound, CredentialBindNotAuthorized, CredentialEntryAlreadyAliased, CredentialInvalidEntry, CredentialNotFound, CredentialSupersededBranch } from './errors.js';
|
|
5
|
+
import { assertValidCredentialShape } from './validate.js';
|
|
5
6
|
export function createCredentialAPI(params) {
|
|
6
7
|
const getAliasStore = ()=>getCredentialStore(params.stores);
|
|
7
8
|
const getCredentials = ()=>params.getCredentialManager(params.stores);
|
|
@@ -13,8 +14,13 @@ export function createCredentialAPI(params) {
|
|
|
13
14
|
const store = await getAliasStore();
|
|
14
15
|
const entryID = await store.getAliasEntryID(providerName, normalizeDID(ownerDID));
|
|
15
16
|
if (entryID == null) return null;
|
|
17
|
+
// A dangling alias (its entry was deleted out from under it) resolves to null,
|
|
18
|
+
// not an error: a missing ENTRY is checked structurally here so a missing KEY
|
|
19
|
+
// inside readEntry still surfaces as CredentialNotFound below.
|
|
20
|
+
if (await store.getEntry(entryID) == null) return null;
|
|
16
21
|
try {
|
|
17
|
-
|
|
22
|
+
const credentials = await getCredentials();
|
|
23
|
+
return toUTF(await credentials.readEntry(entryID));
|
|
18
24
|
} catch (error) {
|
|
19
25
|
if (error instanceof CredentialSupersededBranch) {
|
|
20
26
|
params.logger.warn('Credential entry {entryID} for provider {providerName} belongs to a superseded branch', {
|
|
@@ -44,7 +50,8 @@ export function createCredentialAPI(params) {
|
|
|
44
50
|
// Opens the key through the server's own wrapping and rewrites the entry
|
|
45
51
|
// under it. No key is minted and no wrapping is touched, so a refresh
|
|
46
52
|
// cannot change who can read the credential.
|
|
47
|
-
|
|
53
|
+
const credentials = await getCredentials();
|
|
54
|
+
await credentials.updateEntry(entryID, fromUTF(credential));
|
|
48
55
|
},
|
|
49
56
|
async createCredential (input) {
|
|
50
57
|
const { providerName, ownerDID, ownerWrappableDID, credential } = input;
|
|
@@ -113,15 +120,104 @@ export function createCredentialAPI(params) {
|
|
|
113
120
|
const store = await getAliasStore();
|
|
114
121
|
const owner = normalizeDID(ownerDID);
|
|
115
122
|
const entryID = await store.getAliasEntryID(providerName, owner);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// Entry first, THEN the pointer. This ordering keeps a concurrent bindAlias
|
|
124
|
+
// from leaving a dangling alias: once the entry is gone, bindAliasIfAbsent's
|
|
125
|
+
// entry-existence-conditioned insert refuses (there is no row to point at),
|
|
126
|
+
// so no new alias can be installed against the entry being removed. The brief
|
|
127
|
+
// window where the old pointer outlives its entry is covered by getCredential/
|
|
128
|
+
// getCredentialRef, which treat a missing entry as null. The pointer and the
|
|
129
|
+
// entry still go together — leaving the entry would keep the token readable by
|
|
130
|
+
// anyone who kept its id after a disconnect.
|
|
131
|
+
// No pointer to delete: there is nothing of ours to remove. An unconditional
|
|
132
|
+
// clear here would only drop an alias a concurrent bindAlias installed between
|
|
133
|
+
// the read above and now, so leave the slot untouched.
|
|
134
|
+
if (entryID == null) return;
|
|
135
|
+
const credentials = await getCredentials();
|
|
136
|
+
await credentials.deleteEntry(entryID);
|
|
137
|
+
// Clear the pointer ONLY while it still names the entry we removed. A concurrent
|
|
138
|
+
// bindAlias can repair a briefly-dangling pointer onto a fresh entry in the gap
|
|
139
|
+
// after deleteEntry; an unconditional delete would drop that valid new binding.
|
|
140
|
+
// If the pointer already moved, leave it.
|
|
141
|
+
await store.deleteAliasIfEntry(providerName, owner, entryID);
|
|
142
|
+
},
|
|
143
|
+
async bindAlias (providerName, ownerDID, entryID) {
|
|
144
|
+
const owner = normalizeDID(ownerDID);
|
|
145
|
+
const credentials = await getCredentials();
|
|
146
|
+
// Gate B: the acting subject must own the entry's key. The vault can
|
|
147
|
+
// decrypt any entry it holds a server wrapping for, so decryptability is
|
|
148
|
+
// not authority.
|
|
149
|
+
const keyOwner = await credentials.getEntryKeyOwnerDID(entryID);
|
|
150
|
+
if (keyOwner == null) {
|
|
151
|
+
throw new CredentialNotFound('Credential entry', entryID);
|
|
152
|
+
}
|
|
153
|
+
if (normalizeDID(keyOwner) !== owner) {
|
|
154
|
+
throw new CredentialBindNotAuthorized(providerName, ownerDID);
|
|
155
|
+
}
|
|
156
|
+
const store = await getAliasStore();
|
|
157
|
+
// Idempotent short-circuit BEFORE decrypt, so a safe retry cannot fail on
|
|
158
|
+
// a branch/version check inside readEntry.
|
|
159
|
+
if (await store.getAliasEntryID(providerName, owner) === entryID) {
|
|
160
|
+
return;
|
|
121
161
|
}
|
|
162
|
+
// Validate the decrypted plaintext is actually a credential before it can
|
|
163
|
+
// ever be resolved back out through getCredential.
|
|
164
|
+
const bytes = await credentials.readEntry(entryID);
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(toUTF(bytes));
|
|
168
|
+
} catch (cause) {
|
|
169
|
+
throw new CredentialInvalidEntry('credential entry is not valid JSON', {
|
|
170
|
+
cause
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
assertValidCredentialShape(parsed);
|
|
174
|
+
const result = await store.bindAliasIfAbsent({
|
|
175
|
+
namespace: providerName,
|
|
176
|
+
aliasKey: owner,
|
|
177
|
+
entryID
|
|
178
|
+
});
|
|
179
|
+
if (result === 'inserted') return;
|
|
180
|
+
if (result === 'entry-missing') {
|
|
181
|
+
throw new CredentialNotFound('Credential entry', entryID);
|
|
182
|
+
}
|
|
183
|
+
if (result.conflict === 'alias') {
|
|
184
|
+
if (result.existing === entryID) return; // concurrent idempotent retry
|
|
185
|
+
throw new CredentialAliasAlreadyBound(providerName, ownerDID);
|
|
186
|
+
}
|
|
187
|
+
throw new CredentialEntryAlreadyAliased(providerName, entryID) // conflict: 'entry'
|
|
188
|
+
;
|
|
122
189
|
},
|
|
123
190
|
getControllerMethods () {
|
|
124
191
|
return params.getControllerMethods(params.stores);
|
|
192
|
+
},
|
|
193
|
+
async getCredentialRef (providerName, ownerDID) {
|
|
194
|
+
const store = await getAliasStore();
|
|
195
|
+
const entryID = await store.getAliasEntryID(providerName, normalizeDID(ownerDID));
|
|
196
|
+
if (entryID == null) return null;
|
|
197
|
+
// Dangling alias → null, checked structurally (see getCredential): a missing
|
|
198
|
+
// entry is not an error, a missing key inside readEntry still throws.
|
|
199
|
+
if (await store.getEntry(entryID) == null) return null;
|
|
200
|
+
try {
|
|
201
|
+
const credentials = await getCredentials();
|
|
202
|
+
const bytes = await credentials.readEntry(entryID);
|
|
203
|
+
return {
|
|
204
|
+
credential: toUTF(bytes),
|
|
205
|
+
entryID
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof CredentialSupersededBranch) {
|
|
209
|
+
params.logger.warn('Credential entry {entryID} for provider {providerName} belongs to a superseded branch', {
|
|
210
|
+
providerName,
|
|
211
|
+
entryID
|
|
212
|
+
});
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async setCredentialForEntry (params) {
|
|
219
|
+
const credentials = await getCredentials();
|
|
220
|
+
return await credentials.updateEntryIfPresent(params.entryID, fromUTF(params.credential));
|
|
125
221
|
}
|
|
126
222
|
};
|
|
127
223
|
}
|
package/lib/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { CredentialAuthorization } from './authority.js';
|
|
|
6
6
|
* without deciding what a caller is meant to match on. Messages are for people
|
|
7
7
|
* and change; these do not.
|
|
8
8
|
*/
|
|
9
|
-
export type CredentialErrorCode = 'CREDENTIAL_ERROR' | 'CREDENTIAL_FACTOR_UNAVAILABLE' | 'CREDENTIAL_FACTORS_INVALID' | 'CREDENTIAL_KEY_LOCKED' | 'CREDENTIAL_KEY_VERSION_MISMATCH' | 'CREDENTIAL_NOT_AUTHORIZED' | 'CREDENTIAL_NOT_FOUND' | 'CREDENTIAL_PIN_REJECTED' | 'CREDENTIAL_SECRET_REJECTED' | 'CREDENTIAL_SUPERSEDED_BRANCH' | 'CREDENTIAL_SUITE_UNSUPPORTED' | 'CREDENTIAL_TAMPERED' | 'CREDENTIAL_UNOPENABLE' | 'CREDENTIAL_WRAPPING_AMBIGUOUS';
|
|
9
|
+
export type CredentialErrorCode = 'CREDENTIAL_ALIAS_ALREADY_BOUND' | 'CREDENTIAL_BIND_NOT_AUTHORIZED' | 'CREDENTIAL_ENTRY_ALREADY_ALIASED' | 'CREDENTIAL_ERROR' | 'CREDENTIAL_FACTOR_UNAVAILABLE' | 'CREDENTIAL_FACTORS_INVALID' | 'CREDENTIAL_INVALID_ENTRY' | 'CREDENTIAL_KEY_LOCKED' | 'CREDENTIAL_KEY_VERSION_MISMATCH' | 'CREDENTIAL_NOT_AUTHORIZED' | 'CREDENTIAL_NOT_FOUND' | 'CREDENTIAL_PIN_REJECTED' | 'CREDENTIAL_SECRET_REJECTED' | 'CREDENTIAL_SUPERSEDED_BRANCH' | 'CREDENTIAL_SUITE_UNSUPPORTED' | 'CREDENTIAL_TAMPERED' | 'CREDENTIAL_UNOPENABLE' | 'CREDENTIAL_WRAPPING_AMBIGUOUS';
|
|
10
10
|
export type CredentialErrorOptions = ErrorOptions & {
|
|
11
11
|
code: CredentialErrorCode;
|
|
12
12
|
};
|
|
@@ -167,3 +167,52 @@ export declare class CredentialUnopenable extends CredentialError {
|
|
|
167
167
|
constructor(keyID: string, reason: string);
|
|
168
168
|
get keyID(): string;
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The caller is not the entry's key owner, so the alias bind is refused.
|
|
172
|
+
*
|
|
173
|
+
* Distinct from the existing {@link CredentialNotAuthorized}, which is a
|
|
174
|
+
* verb-scoped key-authority check (`'read' | 'administer'` against a keyID).
|
|
175
|
+
* This is an owner-identity check on a (providerName, ownerDID) pair ahead of
|
|
176
|
+
* the bind-alias seam's Gate B, not a key authority the device may hold —
|
|
177
|
+
* widening `CredentialAuthorization` to cover it would let this check feed
|
|
178
|
+
* `CredentialAuthority.authorize`, which it must not.
|
|
179
|
+
*/
|
|
180
|
+
export declare class CredentialBindNotAuthorized extends CredentialError {
|
|
181
|
+
constructor(providerName: string, ownerDID: string);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A provider/owner alias already points at a different entry.
|
|
185
|
+
*
|
|
186
|
+
* `UNIQUE(namespace, alias_key)` in the alias table enforces this; the bind
|
|
187
|
+
* seam classifies the conflicting read-back into this error rather than
|
|
188
|
+
* silently overwriting the existing binding.
|
|
189
|
+
*/
|
|
190
|
+
export declare class CredentialAliasAlreadyBound extends CredentialError {
|
|
191
|
+
#private;
|
|
192
|
+
constructor(providerName: string, ownerDID: string);
|
|
193
|
+
get providerName(): string;
|
|
194
|
+
get ownerDID(): string;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* An entry already holds an alias, and this bind would give it a second one.
|
|
198
|
+
*
|
|
199
|
+
* `UNIQUE(entry_id)` in the alias table enforces one-alias-per-entry; the bind
|
|
200
|
+
* seam classifies the conflicting read-back into this error rather than
|
|
201
|
+
* attaching a second provider name to the same entry.
|
|
202
|
+
*/
|
|
203
|
+
export declare class CredentialEntryAlreadyAliased extends CredentialError {
|
|
204
|
+
#private;
|
|
205
|
+
constructor(providerName: string, entryID: string);
|
|
206
|
+
get providerName(): string;
|
|
207
|
+
get entryID(): string;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Decrypted entry bytes do not match the `Credential` shape.
|
|
211
|
+
*
|
|
212
|
+
* Bind and refresh decrypt arbitrary stored bytes; without this check a
|
|
213
|
+
* malformed or mistyped entry (`{}`, a non-string `accessToken`) would be
|
|
214
|
+
* trusted as a credential instead of refused at the boundary.
|
|
215
|
+
*/
|
|
216
|
+
export declare class CredentialInvalidEntry extends CredentialError {
|
|
217
|
+
constructor(message: string, options?: ErrorOptions);
|
|
218
|
+
}
|
package/lib/errors.js
CHANGED
|
@@ -246,3 +246,83 @@
|
|
|
246
246
|
return this.#keyID;
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* The caller is not the entry's key owner, so the alias bind is refused.
|
|
251
|
+
*
|
|
252
|
+
* Distinct from the existing {@link CredentialNotAuthorized}, which is a
|
|
253
|
+
* verb-scoped key-authority check (`'read' | 'administer'` against a keyID).
|
|
254
|
+
* This is an owner-identity check on a (providerName, ownerDID) pair ahead of
|
|
255
|
+
* the bind-alias seam's Gate B, not a key authority the device may hold —
|
|
256
|
+
* widening `CredentialAuthorization` to cover it would let this check feed
|
|
257
|
+
* `CredentialAuthority.authorize`, which it must not.
|
|
258
|
+
*/ export class CredentialBindNotAuthorized extends CredentialError {
|
|
259
|
+
constructor(providerName, ownerDID){
|
|
260
|
+
super(`Not authorized to bind ${providerName} alias for ${ownerDID}`, {
|
|
261
|
+
code: 'CREDENTIAL_BIND_NOT_AUTHORIZED'
|
|
262
|
+
});
|
|
263
|
+
this.name = 'CredentialBindNotAuthorized';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* A provider/owner alias already points at a different entry.
|
|
268
|
+
*
|
|
269
|
+
* `UNIQUE(namespace, alias_key)` in the alias table enforces this; the bind
|
|
270
|
+
* seam classifies the conflicting read-back into this error rather than
|
|
271
|
+
* silently overwriting the existing binding.
|
|
272
|
+
*/ export class CredentialAliasAlreadyBound extends CredentialError {
|
|
273
|
+
#providerName;
|
|
274
|
+
#ownerDID;
|
|
275
|
+
constructor(providerName, ownerDID){
|
|
276
|
+
super(`Alias ${providerName} for ${ownerDID} is already bound to a different credential entry`, {
|
|
277
|
+
code: 'CREDENTIAL_ALIAS_ALREADY_BOUND'
|
|
278
|
+
});
|
|
279
|
+
this.name = 'CredentialAliasAlreadyBound';
|
|
280
|
+
this.#providerName = providerName;
|
|
281
|
+
this.#ownerDID = ownerDID;
|
|
282
|
+
}
|
|
283
|
+
get providerName() {
|
|
284
|
+
return this.#providerName;
|
|
285
|
+
}
|
|
286
|
+
get ownerDID() {
|
|
287
|
+
return this.#ownerDID;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* An entry already holds an alias, and this bind would give it a second one.
|
|
292
|
+
*
|
|
293
|
+
* `UNIQUE(entry_id)` in the alias table enforces one-alias-per-entry; the bind
|
|
294
|
+
* seam classifies the conflicting read-back into this error rather than
|
|
295
|
+
* attaching a second provider name to the same entry.
|
|
296
|
+
*/ export class CredentialEntryAlreadyAliased extends CredentialError {
|
|
297
|
+
#providerName;
|
|
298
|
+
#entryID;
|
|
299
|
+
constructor(providerName, entryID){
|
|
300
|
+
super(`Credential entry ${entryID} is already aliased; cannot bind it under ${providerName}`, {
|
|
301
|
+
code: 'CREDENTIAL_ENTRY_ALREADY_ALIASED'
|
|
302
|
+
});
|
|
303
|
+
this.name = 'CredentialEntryAlreadyAliased';
|
|
304
|
+
this.#providerName = providerName;
|
|
305
|
+
this.#entryID = entryID;
|
|
306
|
+
}
|
|
307
|
+
get providerName() {
|
|
308
|
+
return this.#providerName;
|
|
309
|
+
}
|
|
310
|
+
get entryID() {
|
|
311
|
+
return this.#entryID;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Decrypted entry bytes do not match the `Credential` shape.
|
|
316
|
+
*
|
|
317
|
+
* Bind and refresh decrypt arbitrary stored bytes; without this check a
|
|
318
|
+
* malformed or mistyped entry (`{}`, a non-string `accessToken`) would be
|
|
319
|
+
* trusted as a credential instead of refused at the boundary.
|
|
320
|
+
*/ export class CredentialInvalidEntry extends CredentialError {
|
|
321
|
+
constructor(message, options){
|
|
322
|
+
super(message, {
|
|
323
|
+
...options,
|
|
324
|
+
code: 'CREDENTIAL_INVALID_ENTRY'
|
|
325
|
+
});
|
|
326
|
+
this.name = 'CredentialInvalidEntry';
|
|
327
|
+
}
|
|
328
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { buildCredentialEntry, buildCredentialKey } from './builder.js';
|
|
|
7
7
|
export type { CredentialEntryDigestInput, CredentialKeyDigestInput, CredentialWrappingDigestInput, } from './digest.js';
|
|
8
8
|
export { credentialEntriesDigest, credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts, } from './digest.js';
|
|
9
9
|
export type { CredentialErrorCode, CredentialErrorOptions } from './errors.js';
|
|
10
|
-
export { AmbiguousCredentialWrapping, CredentialError, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite, } from './errors.js';
|
|
10
|
+
export { AmbiguousCredentialWrapping, CredentialAliasAlreadyBound, CredentialBindNotAuthorized, CredentialEntryAlreadyAliased, CredentialError, CredentialInvalidEntry, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite, } from './errors.js';
|
|
11
11
|
export type { CreateCredentialManagerParams, CreateKeyParams, CredentialGrantResult, CredentialKeyStatus, CredentialManager, CredentialUnopenableReason, FactorChangeParams, GrantKeyParams, ReKeyEntryParams, RotateKeyParams, } from './manager.js';
|
|
12
12
|
export { createCredentialManager } from './manager.js';
|
|
13
13
|
export type { CredentialOpClaim, CredentialOpSigner, CredentialOpSubject, CredentialPutSubject, CredentialRowSubject, VerifiedCredentialOp, } from './op.js';
|
|
@@ -22,4 +22,5 @@ export type { RefreshEvents, RefreshedEvent, RefreshFailedEvent, RefreshFailureC
|
|
|
22
22
|
export { RefreshingCredentialProvider } from './refreshing-credential-provider.js';
|
|
23
23
|
export type { Argon2Cost, CredentialSuite, FactorInput, FactorInputs, SecretFactorKind, } from './suite.js';
|
|
24
24
|
export { DEFAULT_ARGON2_COST, getSuite, isSupportedSuite, SUITE_KUBUN_CRED_1 } from './suite.js';
|
|
25
|
+
export { assertValidCredentialShape } from './validate.js';
|
|
25
26
|
export { CREDENTIAL_ADMINISTER_ACTION, credentialUserResource } from './vocabulary.js';
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { createCredentialAPI } from './api.js';
|
|
|
2
2
|
export { createDeviceAuthority } from './authority.js';
|
|
3
3
|
export { buildCredentialEntry, buildCredentialKey } from './builder.js';
|
|
4
4
|
export { credentialEntriesDigest, credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts } from './digest.js';
|
|
5
|
-
export { AmbiguousCredentialWrapping, CredentialError, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite } from './errors.js';
|
|
5
|
+
export { AmbiguousCredentialWrapping, CredentialAliasAlreadyBound, CredentialBindNotAuthorized, CredentialEntryAlreadyAliased, CredentialError, CredentialInvalidEntry, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite } from './errors.js';
|
|
6
6
|
export { createCredentialManager } from './manager.js';
|
|
7
7
|
export { compareBranchIDs, createCredentialOpSigner, credentialOpMatchesPut, credentialOpMatchesRow, credentialPutDigest, credentialRowDigest, selectWinningBranch, verifyCredentialOp } from './op.js';
|
|
8
8
|
export { createPinThrottle, DEFAULT_BASE_DELAY, DEFAULT_FREE_ATTEMPTS, DEFAULT_MAX_DELAY } from './pin-throttle.js';
|
|
@@ -10,4 +10,5 @@ export { entryProvenanceFields } from './provenance.js';
|
|
|
10
10
|
export { DBCredentialProvider, deserializeCredential, serializeCredential } from './provider.js';
|
|
11
11
|
export { RefreshingCredentialProvider } from './refreshing-credential-provider.js';
|
|
12
12
|
export { DEFAULT_ARGON2_COST, getSuite, isSupportedSuite, SUITE_KUBUN_CRED_1 } from './suite.js';
|
|
13
|
+
export { assertValidCredentialShape } from './validate.js';
|
|
13
14
|
export { CREDENTIAL_ADMINISTER_ACTION, credentialUserResource } from './vocabulary.js';
|
package/lib/manager.d.ts
CHANGED
|
@@ -257,8 +257,22 @@ export type CredentialManager = {
|
|
|
257
257
|
deleteKey(keyID: string): Promise<void>;
|
|
258
258
|
putEntry(keyID: string, plaintext: Uint8Array): Promise<string>;
|
|
259
259
|
updateEntry(entryID: string, plaintext: Uint8Array): Promise<void>;
|
|
260
|
+
/**
|
|
261
|
+
* As {@link updateEntry}, but never inserts: an entry gone by the time the
|
|
262
|
+
* write lands reports `'absent'` instead of raising. For a writeback racing a
|
|
263
|
+
* concurrent delete, where the caller has no `keyID` to fall back to `putEntry`
|
|
264
|
+
* with and raising would just be a `catch` the caller re-swallows.
|
|
265
|
+
*/
|
|
266
|
+
updateEntryIfPresent(entryID: string, plaintext: Uint8Array): Promise<'updated' | 'absent'>;
|
|
260
267
|
readEntry(entryID: string): Promise<Uint8Array>;
|
|
261
268
|
getEntryProvenance(entryID: string): Promise<CredentialEntryProvenance>;
|
|
269
|
+
/**
|
|
270
|
+
* The RAW `owner_did` off the entry's key row — never normalized. Callers
|
|
271
|
+
* comparing against another DID normalize it themselves; folding it here
|
|
272
|
+
* would make the answer wrong for whichever caller wanted the unfolded form.
|
|
273
|
+
* `null` when the entry, or the key it sits on, is absent.
|
|
274
|
+
*/
|
|
275
|
+
getEntryKeyOwnerDID(entryID: string): Promise<string | null>;
|
|
262
276
|
listEntries(keyID: string): Promise<Array<string>>;
|
|
263
277
|
deleteEntry(entryID: string): Promise<void>;
|
|
264
278
|
/**
|
package/lib/manager.js
CHANGED
|
@@ -898,6 +898,43 @@ export function createCredentialManager(params) {
|
|
|
898
898
|
};
|
|
899
899
|
await store.putEntry(updated, await ops.entry(updated, branchID));
|
|
900
900
|
},
|
|
901
|
+
async updateEntryIfPresent (entryID, plaintext) {
|
|
902
|
+
const existing = await store.getEntry(entryID);
|
|
903
|
+
if (existing == null) {
|
|
904
|
+
return 'absent';
|
|
905
|
+
}
|
|
906
|
+
const { contentKey, version, branchID } = await openKey(existing.key_id);
|
|
907
|
+
// Same re-encrypt-under-current-version shape as `updateEntry`: the suite
|
|
908
|
+
// lookup's store read stays inside the `try` so a rejection cannot leak the
|
|
909
|
+
// key copy.
|
|
910
|
+
let encrypted;
|
|
911
|
+
try {
|
|
912
|
+
const suite = getSuite((await store.getKey(existing.key_id))?.suite ?? SUITE_KUBUN_CRED_1);
|
|
913
|
+
encrypted = suite.encryptEntry({
|
|
914
|
+
runtime,
|
|
915
|
+
contentKey,
|
|
916
|
+
plaintext,
|
|
917
|
+
keyID: existing.key_id,
|
|
918
|
+
keyVersion: version,
|
|
919
|
+
entryID
|
|
920
|
+
});
|
|
921
|
+
} finally{
|
|
922
|
+
contentKey.fill(0);
|
|
923
|
+
}
|
|
924
|
+
const { iv, ciphertext } = encrypted;
|
|
925
|
+
const updated = {
|
|
926
|
+
entryID,
|
|
927
|
+
keyID: existing.key_id,
|
|
928
|
+
keyVersion: version,
|
|
929
|
+
iv,
|
|
930
|
+
ciphertext,
|
|
931
|
+
// A fresh stamp every time, not `existing.hlc` carried forward: the store
|
|
932
|
+
// primitive is op-first and gates the row write on this op winning LWW, so
|
|
933
|
+
// the writeback needs a current stamp to land at all.
|
|
934
|
+
hlc: await nextHLC()
|
|
935
|
+
};
|
|
936
|
+
return await store.updateEntryIfPresent(updated, await ops.entry(updated, branchID));
|
|
937
|
+
},
|
|
901
938
|
async readEntry (entryID) {
|
|
902
939
|
const entry = await store.getEntry(entryID);
|
|
903
940
|
if (entry == null) {
|
|
@@ -943,6 +980,14 @@ export function createCredentialManager(params) {
|
|
|
943
980
|
}
|
|
944
981
|
return entryProvenanceFields(entry);
|
|
945
982
|
},
|
|
983
|
+
async getEntryKeyOwnerDID (entryID) {
|
|
984
|
+
const entry = await store.getEntry(entryID);
|
|
985
|
+
if (entry == null) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const key = await store.getKey(entry.key_id);
|
|
989
|
+
return key == null ? null : key.owner_did;
|
|
990
|
+
},
|
|
946
991
|
async listEntries (keyID) {
|
|
947
992
|
return (await store.listEntries(keyID)).map((entry)=>entry.entry_id);
|
|
948
993
|
},
|
package/lib/provider.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Credential, CredentialProvider } from '@kubun/credential-types';
|
|
1
|
+
import type { AliasBindingCredentialProvider, Credential, CredentialProvider, EntryPairedCredentialProvider } from '@kubun/credential-types';
|
|
2
2
|
import type { CredentialAPI } from './api.js';
|
|
3
3
|
export declare function serializeCredential(credential: Credential): string;
|
|
4
4
|
export declare function deserializeCredential(raw: string | Record<string, unknown>): Credential;
|
|
5
5
|
export type DBCredentialProviderParams = {
|
|
6
6
|
api: CredentialAPI;
|
|
7
7
|
};
|
|
8
|
-
export declare class DBCredentialProvider implements CredentialProvider {
|
|
8
|
+
export declare class DBCredentialProvider implements CredentialProvider, AliasBindingCredentialProvider, EntryPairedCredentialProvider {
|
|
9
9
|
#private;
|
|
10
10
|
constructor(params: DBCredentialProviderParams);
|
|
11
11
|
get(providerName: string, ownerDID: string): Promise<Credential | null>;
|
|
@@ -17,4 +17,15 @@ export declare class DBCredentialProvider implements CredentialProvider {
|
|
|
17
17
|
controllerWrappableDID?: string;
|
|
18
18
|
}): Promise<void>;
|
|
19
19
|
delete(providerName: string, ownerDID: string): Promise<void>;
|
|
20
|
+
bindAlias(providerName: string, ownerDID: string, entryID: string): Promise<void>;
|
|
21
|
+
getCredentialRef(providerName: string, ownerDID: string): Promise<{
|
|
22
|
+
credential: Credential;
|
|
23
|
+
entryID: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
setCredentialForEntry(params: {
|
|
26
|
+
providerName: string;
|
|
27
|
+
ownerDID: string;
|
|
28
|
+
entryID: string;
|
|
29
|
+
credential: Credential;
|
|
30
|
+
}): Promise<'updated' | 'absent'>;
|
|
20
31
|
}
|
package/lib/provider.js
CHANGED
|
@@ -72,4 +72,23 @@ export class DBCredentialProvider {
|
|
|
72
72
|
async delete(providerName, ownerDID) {
|
|
73
73
|
await this.#api.deleteCredential(providerName, ownerDID);
|
|
74
74
|
}
|
|
75
|
+
bindAlias(providerName, ownerDID, entryID) {
|
|
76
|
+
return this.#api.bindAlias(providerName, ownerDID, entryID);
|
|
77
|
+
}
|
|
78
|
+
async getCredentialRef(providerName, ownerDID) {
|
|
79
|
+
const ref = await this.#api.getCredentialRef(providerName, ownerDID);
|
|
80
|
+
if (ref == null) return null;
|
|
81
|
+
return {
|
|
82
|
+
credential: deserializeCredential(ref.credential),
|
|
83
|
+
entryID: ref.entryID
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async setCredentialForEntry(params) {
|
|
87
|
+
return await this.#api.setCredentialForEntry({
|
|
88
|
+
providerName: params.providerName,
|
|
89
|
+
ownerDID: params.ownerDID,
|
|
90
|
+
entryID: params.entryID,
|
|
91
|
+
credential: serializeCredential(params.credential)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
75
94
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { OAuthProviderDefinition } from '@kokuin/oauth';
|
|
2
|
-
import type { Credential, CredentialProvider } from '@kubun/credential-types';
|
|
2
|
+
import type { AliasBindingCredentialProvider, Credential, CredentialProvider, EntryPairedCredentialProvider } from '@kubun/credential-types';
|
|
3
3
|
import type { Logger } from '@kubun/logger';
|
|
4
4
|
import { EventEmitter } from '@sozai/event';
|
|
5
5
|
import type { Runtime } from '@sozai/runtime';
|
|
6
|
-
export type RefreshFailureCategory = 'invalid_grant' | 'transient' | 'configuration' | 'persistence';
|
|
6
|
+
export type RefreshFailureCategory = 'invalid_grant' | 'transient' | 'configuration' | 'persistence' | 'stale';
|
|
7
7
|
export type RefreshedEvent = {
|
|
8
8
|
providerName: string;
|
|
9
9
|
ownerDID: string;
|
|
@@ -27,14 +27,14 @@ export type RefreshOutcome = {
|
|
|
27
27
|
category: RefreshFailureCategory;
|
|
28
28
|
};
|
|
29
29
|
export type RefreshingCredentialProviderParams = {
|
|
30
|
-
inner: CredentialProvider;
|
|
30
|
+
inner: CredentialProvider & EntryPairedCredentialProvider & AliasBindingCredentialProvider;
|
|
31
31
|
runtime: Runtime;
|
|
32
32
|
providers: Array<OAuthProviderDefinition>;
|
|
33
33
|
bufferSeconds?: number;
|
|
34
34
|
logger?: Logger;
|
|
35
35
|
refreshInFlight?: Map<string, Promise<RefreshOutcome>>;
|
|
36
36
|
};
|
|
37
|
-
export declare class RefreshingCredentialProvider implements CredentialProvider {
|
|
37
|
+
export declare class RefreshingCredentialProvider implements CredentialProvider, AliasBindingCredentialProvider {
|
|
38
38
|
#private;
|
|
39
39
|
constructor(params: RefreshingCredentialProviderParams);
|
|
40
40
|
get refreshEvents(): EventEmitter<RefreshEvents>;
|
|
@@ -48,4 +48,5 @@ export declare class RefreshingCredentialProvider implements CredentialProvider
|
|
|
48
48
|
controllerWrappableDID?: string;
|
|
49
49
|
}): Promise<void>;
|
|
50
50
|
delete(providerName: string, ownerDID: string): Promise<void>;
|
|
51
|
+
bindAlias(providerName: string, ownerDID: string, entryID: string): Promise<void>;
|
|
51
52
|
}
|
|
@@ -38,10 +38,11 @@ export class RefreshingCredentialProvider {
|
|
|
38
38
|
return this.#inner.get(providerName, ownerDID);
|
|
39
39
|
}
|
|
40
40
|
async get(providerName, ownerDID) {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
41
|
+
const ref = await this.#inner.getCredentialRef(providerName, ownerDID);
|
|
42
|
+
if (ref == null) return null;
|
|
43
|
+
const { credential, entryID } = ref;
|
|
43
44
|
if (this.#isExpiringSoon(credential) && credential.refreshToken != null) {
|
|
44
|
-
const refreshed = await this.#refresh(providerName, ownerDID, credential, credential.refreshToken);
|
|
45
|
+
const refreshed = await this.#refresh(providerName, ownerDID, entryID, credential, credential.refreshToken);
|
|
45
46
|
return refreshed ?? credential;
|
|
46
47
|
}
|
|
47
48
|
return credential;
|
|
@@ -52,6 +53,12 @@ export class RefreshingCredentialProvider {
|
|
|
52
53
|
delete(providerName, ownerDID) {
|
|
53
54
|
return this.#inner.delete(providerName, ownerDID);
|
|
54
55
|
}
|
|
56
|
+
// Passthrough: alias binding points an owner at an already-existing entry
|
|
57
|
+
// without minting or refreshing anything, so it needs none of this
|
|
58
|
+
// decorator's expiry/refresh logic.
|
|
59
|
+
bindAlias(providerName, ownerDID, entryID) {
|
|
60
|
+
return this.#inner.bindAlias(providerName, ownerDID, entryID);
|
|
61
|
+
}
|
|
55
62
|
#isExpiringSoon(credential) {
|
|
56
63
|
if (credential.expiresAt == null) return false;
|
|
57
64
|
return credential.expiresAt.getTime() - Date.now() < this.#bufferMs;
|
|
@@ -67,14 +74,18 @@ export class RefreshingCredentialProvider {
|
|
|
67
74
|
// `get`). Followers await the coordinator's promise and reuse its outcome for
|
|
68
75
|
// their own stale-fallback decision without emitting a second event. The key is
|
|
69
76
|
// normalized so the two `did:peer:4` spellings of one owner share an entry.
|
|
70
|
-
async #refresh(providerName, ownerDID, credential, refreshToken) {
|
|
71
|
-
|
|
77
|
+
async #refresh(providerName, ownerDID, entryID, credential, refreshToken) {
|
|
78
|
+
// entryID is part of the key: a caller that read a DIFFERENT entry (a
|
|
79
|
+
// delete+rebind landed between its read and this one) must never join an
|
|
80
|
+
// in-flight exchange started against the old entry and receive its
|
|
81
|
+
// refreshed credential.
|
|
82
|
+
const key = `${providerName}:${normalizeDID(ownerDID)}:${entryID}`;
|
|
72
83
|
const inFlight = this.#refreshInFlight.get(key);
|
|
73
84
|
if (inFlight != null) {
|
|
74
85
|
const outcome = await inFlight;
|
|
75
86
|
return outcome.ok ? outcome.credential : null;
|
|
76
87
|
}
|
|
77
|
-
const exchange = this.#exchange(providerName, ownerDID, credential, refreshToken);
|
|
88
|
+
const exchange = this.#exchange(providerName, ownerDID, entryID, credential, refreshToken);
|
|
78
89
|
this.#refreshInFlight.set(key, exchange);
|
|
79
90
|
let outcome;
|
|
80
91
|
try {
|
|
@@ -102,7 +113,7 @@ export class RefreshingCredentialProvider {
|
|
|
102
113
|
// one provider+owner never reach here twice: `#refresh` shares a single pending
|
|
103
114
|
// `#exchange` promise through the single-flight map. Returns a discriminated
|
|
104
115
|
// outcome and does not emit or fall back itself.
|
|
105
|
-
async #exchange(providerName, ownerDID, credential, refreshToken) {
|
|
116
|
+
async #exchange(providerName, ownerDID, entryID, credential, refreshToken) {
|
|
106
117
|
const provider = this.#providers.find((p)=>p.name === providerName);
|
|
107
118
|
if (provider == null) {
|
|
108
119
|
return {
|
|
@@ -137,10 +148,12 @@ export class RefreshingCredentialProvider {
|
|
|
137
148
|
accountLabel: credential.accountLabel,
|
|
138
149
|
metadata: credential.metadata
|
|
139
150
|
};
|
|
151
|
+
let write;
|
|
140
152
|
try {
|
|
141
|
-
await this.#inner.
|
|
153
|
+
write = await this.#inner.setCredentialForEntry({
|
|
142
154
|
providerName,
|
|
143
155
|
ownerDID,
|
|
156
|
+
entryID,
|
|
144
157
|
credential: refreshed
|
|
145
158
|
});
|
|
146
159
|
} catch {
|
|
@@ -155,6 +168,17 @@ export class RefreshingCredentialProvider {
|
|
|
155
168
|
category: rotated ? 'invalid_grant' : 'persistence'
|
|
156
169
|
};
|
|
157
170
|
}
|
|
171
|
+
// The entry read at the start of this refresh is gone (a delete+rebind
|
|
172
|
+
// landed mid-exchange) — checked BEFORE any rotation reasoning, because
|
|
173
|
+
// this is not a persistence failure or an invalidated refresh token: the
|
|
174
|
+
// user's own delete removed the target, and the refreshed credential must
|
|
175
|
+
// never overwrite whatever now sits at that provider+owner slot.
|
|
176
|
+
if (write === 'absent') {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
category: 'stale'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
158
182
|
return {
|
|
159
183
|
ok: true,
|
|
160
184
|
credential: refreshed
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CredentialInvalidEntry } from './errors.js';
|
|
2
|
+
// Bind/refresh decrypt arbitrary stored bytes; deserializeCredential only JSON.parses
|
|
3
|
+
// and casts, so `{}` or mistyped fields would pass. Validate against the typed
|
|
4
|
+
// contract before trusting an entry as a credential.
|
|
5
|
+
export function assertValidCredentialShape(value) {
|
|
6
|
+
if (value == null || typeof value !== 'object') {
|
|
7
|
+
throw new CredentialInvalidEntry('credential is not an object');
|
|
8
|
+
}
|
|
9
|
+
const c = value;
|
|
10
|
+
if (typeof c.accessToken !== 'string' || c.accessToken.length === 0) {
|
|
11
|
+
throw new CredentialInvalidEntry('accessToken must be a non-empty string');
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(c.scopes) || c.scopes.some((s)=>typeof s !== 'string')) {
|
|
14
|
+
throw new CredentialInvalidEntry('scopes must be an array of strings');
|
|
15
|
+
}
|
|
16
|
+
if (c.refreshToken != null && typeof c.refreshToken !== 'string') {
|
|
17
|
+
throw new CredentialInvalidEntry('refreshToken must be a string when present');
|
|
18
|
+
}
|
|
19
|
+
if (c.accountLabel != null && typeof c.accountLabel !== 'string') {
|
|
20
|
+
throw new CredentialInvalidEntry('accountLabel must be a string when present');
|
|
21
|
+
}
|
|
22
|
+
// expiresAt is serialized as an ISO string; an unparseable one deserializes to
|
|
23
|
+
// an Invalid Date, which silently disables the refresh comparison. Reject it here.
|
|
24
|
+
if (c.expiresAt != null && (typeof c.expiresAt !== 'string' || Number.isNaN(Date.parse(c.expiresAt)))) {
|
|
25
|
+
throw new CredentialInvalidEntry('expiresAt must be an ISO date string when present');
|
|
26
|
+
}
|
|
27
|
+
if (c.metadata != null && (typeof c.metadata !== 'object' || Array.isArray(c.metadata))) {
|
|
28
|
+
throw new CredentialInvalidEntry('metadata must be an object when present');
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/credential",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"@kokuin/jwe": "^0.1.0",
|
|
19
19
|
"@kokuin/oauth": "^0.1.0",
|
|
20
20
|
"@kokuin/token": "^0.5.0",
|
|
21
|
-
"@kubun/credential-types": "^0.17.
|
|
21
|
+
"@kubun/credential-types": "^0.17.1",
|
|
22
22
|
"@kubun/db": "^0.17.0",
|
|
23
23
|
"@kubun/hlc": "^0.17.0",
|
|
24
24
|
"@kubun/id": "^0.17.0",
|
|
25
25
|
"@kubun/logger": "^0.17.0",
|
|
26
|
-
"@kubun/store-credential": "^0.17.
|
|
26
|
+
"@kubun/store-credential": "^0.17.4",
|
|
27
27
|
"@noble/ciphers": "^2.4.0",
|
|
28
28
|
"@noble/hashes": "^2.4.0",
|
|
29
29
|
"@sozai/codec": "^0.4.0",
|
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
"@sozai/runtime": "^0.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@kubun/db-
|
|
34
|
+
"@kubun/db-adapter": "^0.17.0",
|
|
35
|
+
"@kubun/db-better-sqlite": "^0.17.0",
|
|
36
|
+
"@kubun/db-postgres": "^0.17.1",
|
|
37
|
+
"@testcontainers/postgresql": "^12.1.0",
|
|
38
|
+
"kysely": "^0.29.6"
|
|
35
39
|
},
|
|
36
40
|
"publishConfig": {
|
|
37
41
|
"access": "public"
|