@kubun/plugin-credential 0.17.4 → 0.18.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/context.d.ts +2 -2
- package/lib/context.js +14 -13
- package/lib/schema.js +4 -4
- package/lib/service.d.ts +6 -0
- package/lib/service.js +174 -21
- package/package.json +20 -20
package/lib/context.d.ts
CHANGED
|
@@ -58,12 +58,12 @@ export type PutCredentialEntryInput = {
|
|
|
58
58
|
};
|
|
59
59
|
export type CredentialQueryContext = {
|
|
60
60
|
getKey(keyID: string): Promise<CredentialKeyResult>;
|
|
61
|
-
getEntry(entryID: string): Promise<CredentialEntryResult>;
|
|
61
|
+
getEntry(entryID: string, ownerDID: string): Promise<CredentialEntryResult>;
|
|
62
62
|
listEntries(keyID: string): Promise<Array<string>>;
|
|
63
63
|
putKey(input: PutCredentialKeyInput): Promise<CredentialKeyResult>;
|
|
64
64
|
putEntry(input: PutCredentialEntryInput): Promise<CredentialEntryResult>;
|
|
65
65
|
deleteKey(keyID: string): Promise<boolean>;
|
|
66
|
-
deleteEntry(entryID: string): Promise<boolean>;
|
|
66
|
+
deleteEntry(entryID: string, ownerDID: string): Promise<boolean>;
|
|
67
67
|
};
|
|
68
68
|
export declare function createCredentialQueryContext(params: {
|
|
69
69
|
getStore: () => Promise<CredentialStoreAPI>;
|
package/lib/context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
1
2
|
import { entryProvenanceFields } from '@kubun/credential';
|
|
2
3
|
import { HLC } from '@kubun/hlc';
|
|
3
4
|
import { CredentialEntryID, CredentialKeyID } from '@kubun/id';
|
|
@@ -69,7 +70,7 @@ export function createCredentialQueryContext(params) {
|
|
|
69
70
|
return await requireAuthorized(store, await store.getKey(keyID), keyID, authorization);
|
|
70
71
|
}
|
|
71
72
|
async function keyResult(store, key) {
|
|
72
|
-
const wrappings = await store.listWrappings(key.key_id);
|
|
73
|
+
const wrappings = await store.listWrappings(key.key_id, undefined, key.owner_did);
|
|
73
74
|
return {
|
|
74
75
|
keyID: key.key_id,
|
|
75
76
|
ownerDID: key.owner_did,
|
|
@@ -86,15 +87,15 @@ export function createCredentialQueryContext(params) {
|
|
|
86
87
|
const store = await getStore();
|
|
87
88
|
return await keyResult(store, await loadKey(store, keyID, 'read'));
|
|
88
89
|
},
|
|
89
|
-
async getEntry (entryID) {
|
|
90
|
+
async getEntry (entryID, ownerDID) {
|
|
90
91
|
const store = await getStore();
|
|
91
|
-
const entry = await store.getEntryWithProvenance(entryID);
|
|
92
|
+
const entry = await store.getEntryWithProvenance(entryID, normalizeDID(ownerDID));
|
|
92
93
|
if (entry == null) {
|
|
93
94
|
throw notFound('Credential entry', entryID);
|
|
94
95
|
}
|
|
95
96
|
// An entry carries no authorization of its own. Its key does, and that is
|
|
96
97
|
// the only thing that decides who may read the ciphertext.
|
|
97
|
-
await
|
|
98
|
+
await requireAuthorized(store, await store.getKey(entry.key_id, entry.owner_did), entry.key_id, 'read');
|
|
98
99
|
return toEntryResult(entry);
|
|
99
100
|
},
|
|
100
101
|
async listEntries (keyID) {
|
|
@@ -133,7 +134,7 @@ export function createCredentialQueryContext(params) {
|
|
|
133
134
|
suite: input.suite,
|
|
134
135
|
version: input.version,
|
|
135
136
|
state: 'active'
|
|
136
|
-
}
|
|
137
|
+
});
|
|
137
138
|
for (const wrapping of input.wrappings){
|
|
138
139
|
await store.putWrapping({
|
|
139
140
|
wrappingID: wrapping.wrappingID,
|
|
@@ -143,7 +144,7 @@ export function createCredentialQueryContext(params) {
|
|
|
143
144
|
iv: fromB64(wrapping.iv),
|
|
144
145
|
wrappedKey: fromB64(wrapping.wrappedKey),
|
|
145
146
|
recipientDID: wrapping.recipientDID ?? null
|
|
146
|
-
}
|
|
147
|
+
});
|
|
147
148
|
}
|
|
148
149
|
return await keyResult(store, await loadKey(store, input.keyID, 'administer'));
|
|
149
150
|
},
|
|
@@ -156,7 +157,7 @@ export function createCredentialQueryContext(params) {
|
|
|
156
157
|
const store = await getStore();
|
|
157
158
|
// Administrative, not read: a recipient can decrypt this key's entries,
|
|
158
159
|
// which is not a reason to let it write into someone else's vault.
|
|
159
|
-
await loadKey(store, input.keyID, 'administer');
|
|
160
|
+
const key = await loadKey(store, input.keyID, 'administer');
|
|
160
161
|
await store.putEntry({
|
|
161
162
|
entryID: input.entryID,
|
|
162
163
|
keyID: input.keyID,
|
|
@@ -164,8 +165,8 @@ export function createCredentialQueryContext(params) {
|
|
|
164
165
|
iv: fromB64(input.iv),
|
|
165
166
|
ciphertext: fromB64(input.ciphertext),
|
|
166
167
|
hlc: HLC.serialize(params.hlc.now())
|
|
167
|
-
}
|
|
168
|
-
const written = await store.getEntryWithProvenance(input.entryID);
|
|
168
|
+
});
|
|
169
|
+
const written = await store.getEntryWithProvenance(input.entryID, key.owner_did);
|
|
169
170
|
if (written == null) {
|
|
170
171
|
throw notFound('Credential entry', input.entryID);
|
|
171
172
|
}
|
|
@@ -174,17 +175,17 @@ export function createCredentialQueryContext(params) {
|
|
|
174
175
|
async deleteKey (keyID) {
|
|
175
176
|
const store = await getStore();
|
|
176
177
|
await loadKey(store, keyID, 'administer');
|
|
177
|
-
await store.deleteKey(keyID
|
|
178
|
+
await store.deleteKey(keyID);
|
|
178
179
|
return true;
|
|
179
180
|
},
|
|
180
|
-
async deleteEntry (entryID) {
|
|
181
|
+
async deleteEntry (entryID, ownerDID) {
|
|
181
182
|
const store = await getStore();
|
|
182
|
-
const entry = await store.getEntry(entryID);
|
|
183
|
+
const entry = await store.getEntry(entryID, normalizeDID(ownerDID));
|
|
183
184
|
if (entry == null) {
|
|
184
185
|
throw notFound('Credential entry', entryID);
|
|
185
186
|
}
|
|
186
187
|
await loadKey(store, entry.key_id, 'administer');
|
|
187
|
-
await store.deleteEntry(entryID,
|
|
188
|
+
await store.deleteEntry(entryID, entry.owner_did);
|
|
188
189
|
return true;
|
|
189
190
|
}
|
|
190
191
|
};
|
package/lib/schema.js
CHANGED
|
@@ -61,7 +61,7 @@ input CredentialEntryInput {
|
|
|
61
61
|
|
|
62
62
|
extend type Query {
|
|
63
63
|
credentialKey(keyID: String!): CredentialKey!
|
|
64
|
-
credentialEntry(entryID: String!): CredentialEntry!
|
|
64
|
+
credentialEntry(entryID: String!, ownerDID: String!): CredentialEntry!
|
|
65
65
|
credentialEntries(keyID: String!): [String!]!
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -69,7 +69,7 @@ extend type Mutation {
|
|
|
69
69
|
putCredentialKey(input: CredentialKeyInput!): CredentialKey!
|
|
70
70
|
putCredentialEntry(input: CredentialEntryInput!): CredentialEntry!
|
|
71
71
|
deleteCredentialKey(keyID: String!): Boolean!
|
|
72
|
-
deleteCredentialEntry(entryID: String!): Boolean!
|
|
72
|
+
deleteCredentialEntry(entryID: String!, ownerDID: String!): Boolean!
|
|
73
73
|
}
|
|
74
74
|
`;
|
|
75
75
|
function requireCredential(ctx) {
|
|
@@ -97,14 +97,14 @@ function requireCredential(ctx) {
|
|
|
97
97
|
resolvers: {
|
|
98
98
|
queryFields: {
|
|
99
99
|
credentialKey: (_source, args, context)=>requireCredential(context).getKey(args.keyID),
|
|
100
|
-
credentialEntry: (_source, args, context)=>requireCredential(context).getEntry(args.entryID),
|
|
100
|
+
credentialEntry: (_source, args, context)=>requireCredential(context).getEntry(args.entryID, args.ownerDID),
|
|
101
101
|
credentialEntries: (_source, args, context)=>requireCredential(context).listEntries(args.keyID)
|
|
102
102
|
},
|
|
103
103
|
mutationFields: {
|
|
104
104
|
putCredentialKey: (_source, args, context)=>requireCredential(context).putKey(args.input),
|
|
105
105
|
putCredentialEntry: (_source, args, context)=>requireCredential(context).putEntry(args.input),
|
|
106
106
|
deleteCredentialKey: (_source, args, context)=>requireCredential(context).deleteKey(args.keyID),
|
|
107
|
-
deleteCredentialEntry: (_source, args, context)=>requireCredential(context).deleteEntry(args.entryID)
|
|
107
|
+
deleteCredentialEntry: (_source, args, context)=>requireCredential(context).deleteEntry(args.entryID, args.ownerDID)
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
};
|
package/lib/service.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import type { CreateCredentialManagerParams } from '@kubun/credential';
|
|
1
2
|
import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
|
|
3
|
+
/** Bind the service's recovery preflight and current removal set to each manager. */
|
|
4
|
+
export declare function createServiceCredentialManager(params: Pick<CreateCredentialManagerParams, 'store' | 'identity' | 'runtime' | 'hlc' | 'authority'> & {
|
|
5
|
+
ensure(): Promise<void>;
|
|
6
|
+
removedDevices(ownerDID: string): Promise<ReadonlySet<string>>;
|
|
7
|
+
}): import("@kubun/credential").CredentialManager;
|
|
2
8
|
/**
|
|
3
9
|
* The credential service provider. Unlike {@link createCredentialPlugin}
|
|
4
10
|
* (graph-schema-only, for an engine that consumes credentials), this registers a
|
package/lib/service.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isSigningIdentity, normalizeDID } from '@kokuin/token';
|
|
2
|
-
import { CredentialAliasAlreadyBound, CredentialBindNotAuthorized, CredentialEntryAlreadyAliased, CredentialInvalidEntry, CredentialKeyVersionMismatch, CredentialNotFound, CredentialSupersededBranch, createCredentialAPI, createCredentialManager, wrappableDID } from '@kubun/credential';
|
|
2
|
+
import { CredentialAliasAlreadyBound, CredentialBindNotAuthorized, CredentialEntryAlreadyAliased, CredentialGrantCopyUnavailable, CredentialGrantOwnerAmbiguous, CredentialGrantRehomeNotAuthorized, CredentialInvalidEntry, CredentialKeyVersionMismatch, CredentialNotFound, CredentialSupersededBranch, createCredentialAPI, createCredentialManager, wrappableDID } from '@kubun/credential';
|
|
3
|
+
import { controllerHeadVersion, resolveCurrentControllerOfDevice } from '@kubun/engine';
|
|
3
4
|
import { authorizeRead, authorizeWrite, ServiceAuthorizationError } from '@kubun/plugin-service-server';
|
|
5
|
+
import { credentialServiceProtocol } from '@kubun/service-credential-api';
|
|
4
6
|
import { credentialStoreDefinition, getCredentialStore } from '@kubun/store-credential';
|
|
5
7
|
// The service boundary has no CREDENTIAL_* → KB* mapping; toHandlerError forwards
|
|
6
8
|
// a thrown error's `code` only when it is already a registered KubunErrorCode,
|
|
@@ -22,6 +24,18 @@ function translateCredentialError(err) {
|
|
|
22
24
|
if (err instanceof CredentialNotFound) {
|
|
23
25
|
return withCode(err, 'KB02', 'CREDENTIAL_NOT_FOUND');
|
|
24
26
|
}
|
|
27
|
+
if (err instanceof CredentialGrantCopyUnavailable) {
|
|
28
|
+
return withCode(err, 'KB02', 'CREDENTIAL_GRANT_COPY_UNAVAILABLE');
|
|
29
|
+
}
|
|
30
|
+
if (err instanceof CredentialGrantOwnerAmbiguous) {
|
|
31
|
+
return withCode(err, 'KB04', 'CREDENTIAL_GRANT_OWNER_AMBIGUOUS');
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof CredentialGrantRehomeNotAuthorized) {
|
|
34
|
+
return withCode(err, 'KB07', 'CREDENTIAL_GRANT_REHOME_NOT_AUTHORIZED');
|
|
35
|
+
}
|
|
36
|
+
if (err instanceof Error && 'code' in err && err.code === 'CREDENTIAL_GRANT_ACCOUNT_MISMATCH') {
|
|
37
|
+
return withCode(err, 'KB07', 'CREDENTIAL_GRANT_ACCOUNT_MISMATCH');
|
|
38
|
+
}
|
|
25
39
|
if (err instanceof CredentialInvalidEntry) {
|
|
26
40
|
return withCode(err, 'KB01', 'CREDENTIAL_INVALID_ENTRY');
|
|
27
41
|
}
|
|
@@ -66,6 +80,19 @@ function readAccountPeek(raw) {
|
|
|
66
80
|
providerAccount
|
|
67
81
|
};
|
|
68
82
|
}
|
|
83
|
+
/** Bind the service's recovery preflight and current removal set to each manager. */ export function createServiceCredentialManager(params) {
|
|
84
|
+
return createCredentialManager({
|
|
85
|
+
store: params.store,
|
|
86
|
+
identity: params.identity,
|
|
87
|
+
runtime: params.runtime,
|
|
88
|
+
hlc: params.hlc,
|
|
89
|
+
authority: params.authority,
|
|
90
|
+
removalRecovery: {
|
|
91
|
+
ensure: params.ensure,
|
|
92
|
+
removedDevices: params.removedDevices
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
69
96
|
/**
|
|
70
97
|
* The credential service provider. Unlike {@link createCredentialPlugin}
|
|
71
98
|
* (graph-schema-only, for an engine that consumes credentials), this registers a
|
|
@@ -90,12 +117,45 @@ function readAccountPeek(raw) {
|
|
|
90
117
|
}
|
|
91
118
|
const identity = params.identity;
|
|
92
119
|
const logger = params.getLogger('credential-service');
|
|
93
|
-
const
|
|
120
|
+
const recoveryResolver = params.controllerResolverFor(params.db);
|
|
121
|
+
const resolveRecoveryController = (did)=>resolveCurrentControllerOfDevice(params.db, did, recoveryResolver);
|
|
122
|
+
const recoveryOptions = {
|
|
123
|
+
resolveController: resolveRecoveryController,
|
|
124
|
+
removedDevices: async (ownerDID)=>await recoveryResolver.resolveDenySet?.(ownerDID) ?? new Set(),
|
|
125
|
+
methods: [
|
|
126
|
+
recoveryResolver
|
|
127
|
+
],
|
|
128
|
+
headVersion: async (ownerDID)=>controllerHeadVersion(await params.controllerHead(params.db, ownerDID))
|
|
129
|
+
};
|
|
130
|
+
const getCredentialManager = async (stores, recoveryChecked = false)=>createServiceCredentialManager({
|
|
94
131
|
store: await getCredentialStore(stores),
|
|
95
132
|
identity,
|
|
96
133
|
runtime: params.runtime,
|
|
97
|
-
hlc: params.hlc
|
|
134
|
+
hlc: params.hlc,
|
|
135
|
+
ensure: recoveryChecked ? async ()=>{} : checkRemovalRecovery,
|
|
136
|
+
removedDevices: recoveryOptions.removedDevices
|
|
98
137
|
});
|
|
138
|
+
const recoveryManager = getCredentialStore(params.db).then((store)=>createCredentialManager({
|
|
139
|
+
store,
|
|
140
|
+
identity,
|
|
141
|
+
runtime: params.runtime,
|
|
142
|
+
hlc: params.hlc,
|
|
143
|
+
removalRecovery: recoveryOptions,
|
|
144
|
+
authority: {
|
|
145
|
+
ownerForNewKey: async ()=>await resolveRecoveryController(identity.id) ?? identity.id,
|
|
146
|
+
authorize: async (_action, { key })=>normalizeDID(key.owner_did) === await resolveRecoveryController(identity.id),
|
|
147
|
+
authorizeRevocation: async ({ key })=>normalizeDID(key.owner_did) === await resolveRecoveryController(identity.id)
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
let completedRecoveryHead;
|
|
151
|
+
const checkRemovalRecovery = async ()=>{
|
|
152
|
+
const owner = await resolveRecoveryController(identity.id);
|
|
153
|
+
if (owner === null) return;
|
|
154
|
+
const version = await recoveryOptions.headVersion(owner);
|
|
155
|
+
if (version === completedRecoveryHead) return;
|
|
156
|
+
await (await recoveryManager).recoverRemovedDevices(recoveryOptions);
|
|
157
|
+
completedRecoveryHead = version;
|
|
158
|
+
};
|
|
99
159
|
// The mint folds inside its own write tx (`writeInTx`), so its resolver is
|
|
100
160
|
// local-only: a recipient miss must fail closed, never pull a controller log
|
|
101
161
|
// through `consultProviders` while the write lock is held. The pre-warm below
|
|
@@ -108,19 +168,42 @@ function readAccountPeek(raw) {
|
|
|
108
168
|
// The seam is bound to whatever provider it is built over: reads run against
|
|
109
169
|
// the root, writes against a transaction (below). A manager, alias store and
|
|
110
170
|
// controller resolver all read from that same provider.
|
|
111
|
-
const makeSeam = (stores)=>createCredentialAPI({
|
|
171
|
+
const makeSeam = (stores, recoveryChecked = false)=>createCredentialAPI({
|
|
112
172
|
stores,
|
|
113
173
|
logger,
|
|
114
|
-
getCredentialManager,
|
|
174
|
+
getCredentialManager: (provider, checked)=>getCredentialManager(provider, recoveryChecked || checked === true),
|
|
115
175
|
serverWrappableDID: wrappableDID(params.identity),
|
|
116
|
-
getControllerMethods
|
|
176
|
+
getControllerMethods,
|
|
177
|
+
wrappingRecipientsPrewarmed: true,
|
|
178
|
+
resolveControllerOfOwner: (ownerDID, provider)=>resolveCurrentControllerOfDevice(provider, ownerDID, params.controllerResolverFor(provider, {
|
|
179
|
+
localOnly: true
|
|
180
|
+
}))
|
|
117
181
|
});
|
|
118
182
|
const readSeam = makeSeam(params.db);
|
|
183
|
+
const localDeviceDID = normalizeDID(identity.id);
|
|
184
|
+
const requireLocalGrantAddress = (param)=>{
|
|
185
|
+
if (param.deviceDID === undefined) return;
|
|
186
|
+
try {
|
|
187
|
+
if (normalizeDID(param.deviceDID) === localDeviceDID) return;
|
|
188
|
+
} catch {
|
|
189
|
+
// Invalid device addresses are refused by the same named credential error.
|
|
190
|
+
}
|
|
191
|
+
throw translateCredentialError(new CredentialBindNotAuthorized(param.providerName, param.ownerDID));
|
|
192
|
+
};
|
|
193
|
+
const localRead = async (context, empty, proceed)=>context.iss != null && normalizeDID(context.iss) === localDeviceDID || await context.authorize(localDeviceDID) ? proceed() : empty;
|
|
194
|
+
const localWrite = async (context)=>{
|
|
195
|
+
if ((context.iss == null || normalizeDID(context.iss) !== localDeviceDID) && !await context.authorize(localDeviceDID)) {
|
|
196
|
+
throw new ServiceAuthorizationError('The caller cannot spend this device grant');
|
|
197
|
+
}
|
|
198
|
+
};
|
|
119
199
|
// A mint/refresh/delete is multi-statement (key + wrappings + entry + alias
|
|
120
200
|
// pointer). Outside a graph mutation there is no ambient transaction, so run
|
|
121
201
|
// each write in its own: a mid-mint failure would otherwise leave an
|
|
122
202
|
// orphan key with no alias pointer in the vault.
|
|
123
|
-
const writeInTx = (fn)=>
|
|
203
|
+
const writeInTx = async (fn)=>{
|
|
204
|
+
await checkRemovalRecovery();
|
|
205
|
+
return await params.db.withTransaction((tx)=>fn(makeSeam(tx, true)));
|
|
206
|
+
};
|
|
124
207
|
// A first-time `did:kokuin:` owner (or recovery controller) may hold no log in
|
|
125
208
|
// this engine's controller store yet. Resolve it through the pull-allowed ROOT
|
|
126
209
|
// resolver before the mint tx opens: on a store miss this pulls via the
|
|
@@ -148,7 +231,19 @@ function readAccountPeek(raw) {
|
|
|
148
231
|
context,
|
|
149
232
|
subject: param.ownerDID,
|
|
150
233
|
emptyResult: null,
|
|
151
|
-
proceed: ()=>readSeam.getCredential(param.providerName, param.ownerDID)
|
|
234
|
+
proceed: ()=>localRead(context, null, ()=>readSeam.getCredential(param.providerName, param.ownerDID))
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
'credential/get-ref': (context)=>{
|
|
238
|
+
const param = context.param;
|
|
239
|
+
return authorizeRead({
|
|
240
|
+
context,
|
|
241
|
+
subject: param.ownerDID,
|
|
242
|
+
emptyResult: null,
|
|
243
|
+
proceed: ()=>localRead(context, null, ()=>{
|
|
244
|
+
requireLocalGrantAddress(param);
|
|
245
|
+
return readSeam.getVersionedCredentialRef(param.providerName, param.ownerDID);
|
|
246
|
+
})
|
|
152
247
|
});
|
|
153
248
|
},
|
|
154
249
|
// The same vault read as `credential/get` — the DB credential API, which
|
|
@@ -160,11 +255,11 @@ function readAccountPeek(raw) {
|
|
|
160
255
|
context,
|
|
161
256
|
subject: param.ownerDID,
|
|
162
257
|
emptyResult: null,
|
|
163
|
-
proceed: async ()=>{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
258
|
+
proceed: ()=>localRead(context, null, async ()=>{
|
|
259
|
+
const raw = await readSeam.getCredential(param.providerName, param.ownerDID);
|
|
260
|
+
if (raw == null) return null;
|
|
261
|
+
return readAccountPeek(raw);
|
|
262
|
+
})
|
|
168
263
|
});
|
|
169
264
|
},
|
|
170
265
|
'credential/provenance': (context)=>{
|
|
@@ -173,7 +268,7 @@ function readAccountPeek(raw) {
|
|
|
173
268
|
context,
|
|
174
269
|
subject: param.ownerDID,
|
|
175
270
|
emptyResult: null,
|
|
176
|
-
proceed: ()=>readSeam.getCredentialProvenance(param.providerName, param.ownerDID)
|
|
271
|
+
proceed: ()=>localRead(context, null, ()=>readSeam.getCredentialProvenance(param.providerName, param.ownerDID))
|
|
177
272
|
});
|
|
178
273
|
},
|
|
179
274
|
'credential/create': async (context)=>{
|
|
@@ -182,6 +277,7 @@ function readAccountPeek(raw) {
|
|
|
182
277
|
context,
|
|
183
278
|
subject: param.ownerDID
|
|
184
279
|
});
|
|
280
|
+
await localWrite(context);
|
|
185
281
|
// `ownerWrappableDID` is the wrappable spelling of the SAME identity the
|
|
186
282
|
// authorizer just cleared (`ownerDID`). Nothing downstream re-checks that,
|
|
187
283
|
// so without this a caller authorized for the owner could wrap the token to
|
|
@@ -193,13 +289,24 @@ function readAccountPeek(raw) {
|
|
|
193
289
|
param.ownerWrappableDID,
|
|
194
290
|
param.controllerWrappableDID
|
|
195
291
|
]);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
292
|
+
try {
|
|
293
|
+
await writeInTx((seam)=>seam.createCredential({
|
|
294
|
+
providerName: param.providerName,
|
|
295
|
+
ownerDID: param.ownerDID,
|
|
296
|
+
ownerWrappableDID: param.ownerWrappableDID,
|
|
297
|
+
controllerWrappableDID: param.controllerWrappableDID,
|
|
298
|
+
credential: param.credential,
|
|
299
|
+
authorizeKeyOwnerDID: async (ownerDID)=>{
|
|
300
|
+
await authorizeWrite({
|
|
301
|
+
context,
|
|
302
|
+
subject: ownerDID
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
}));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw translateCredentialError(error);
|
|
309
|
+
}
|
|
203
310
|
return null;
|
|
204
311
|
},
|
|
205
312
|
'credential/refresh': async (context)=>{
|
|
@@ -208,9 +315,44 @@ function readAccountPeek(raw) {
|
|
|
208
315
|
context,
|
|
209
316
|
subject: param.ownerDID
|
|
210
317
|
});
|
|
318
|
+
await localWrite(context);
|
|
211
319
|
await writeInTx((seam)=>seam.setCredential(param.providerName, param.ownerDID, param.credential));
|
|
212
320
|
return null;
|
|
213
321
|
},
|
|
322
|
+
'credential/refresh-entry': async (context)=>{
|
|
323
|
+
const param = context.param;
|
|
324
|
+
await authorizeWrite({
|
|
325
|
+
context,
|
|
326
|
+
subject: param.ownerDID
|
|
327
|
+
});
|
|
328
|
+
await localWrite(context);
|
|
329
|
+
requireLocalGrantAddress(param);
|
|
330
|
+
return writeInTx(async (seam)=>{
|
|
331
|
+
// The authorized alias must still name the entry being refreshed.
|
|
332
|
+
const ref = await seam.getVersionedCredentialRef(param.providerName, param.ownerDID);
|
|
333
|
+
if (ref?.entryID !== param.entryID) return 'absent';
|
|
334
|
+
return param.expectedVersion == null ? seam.setCredentialForEntry(param) : seam.setCredentialForEntryIfUnchanged({
|
|
335
|
+
...param,
|
|
336
|
+
expectedVersion: param.expectedVersion
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
'credential/adopt-grant': async (context)=>{
|
|
341
|
+
const param = context.param;
|
|
342
|
+
if (normalizeDID(param.toDeviceDID) !== localDeviceDID) {
|
|
343
|
+
throw new ServiceAuthorizationError('The destination must be this device');
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const keyOwnerDID = await readSeam.getGrantKeyOwnerDID(param);
|
|
347
|
+
await authorizeWrite({
|
|
348
|
+
context,
|
|
349
|
+
subject: keyOwnerDID
|
|
350
|
+
});
|
|
351
|
+
return await readSeam.adoptCredentialGrant(param, keyOwnerDID);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw translateCredentialError(error);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
214
356
|
'credential/delete': async (context)=>{
|
|
215
357
|
const param = context.param;
|
|
216
358
|
await authorizeWrite({
|
|
@@ -226,6 +368,7 @@ function readAccountPeek(raw) {
|
|
|
226
368
|
context,
|
|
227
369
|
subject: param.ownerDID
|
|
228
370
|
}); // Gate A
|
|
371
|
+
await localWrite(context);
|
|
229
372
|
try {
|
|
230
373
|
await writeInTx((seam)=>seam.bindAlias(param.providerName, param.ownerDID, param.entryID));
|
|
231
374
|
} catch (err) {
|
|
@@ -237,10 +380,14 @@ function readAccountPeek(raw) {
|
|
|
237
380
|
params.registerProvider('service', {
|
|
238
381
|
name: 'credential',
|
|
239
382
|
handlers,
|
|
383
|
+
protocol: credentialServiceProtocol,
|
|
240
384
|
accessRules: {
|
|
241
385
|
'credential/get': {
|
|
242
386
|
allow: true
|
|
243
387
|
},
|
|
388
|
+
'credential/get-ref': {
|
|
389
|
+
allow: true
|
|
390
|
+
},
|
|
244
391
|
'credential/peek': {
|
|
245
392
|
allow: true
|
|
246
393
|
},
|
|
@@ -253,6 +400,12 @@ function readAccountPeek(raw) {
|
|
|
253
400
|
'credential/refresh': {
|
|
254
401
|
allow: true
|
|
255
402
|
},
|
|
403
|
+
'credential/refresh-entry': {
|
|
404
|
+
allow: true
|
|
405
|
+
},
|
|
406
|
+
'credential/adopt-grant': {
|
|
407
|
+
allow: true
|
|
408
|
+
},
|
|
256
409
|
'credential/delete': {
|
|
257
410
|
allow: true
|
|
258
411
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-credential",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -14,30 +14,30 @@
|
|
|
14
14
|
"LICENSE.md"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@kokuin/token": "^0.5.
|
|
18
|
-
"@kubun/credential": "^0.
|
|
19
|
-
"@kubun/db": "^0.
|
|
20
|
-
"@kubun/engine": "^0.
|
|
21
|
-
"@kubun/graphql": "^0.
|
|
22
|
-
"@kubun/hlc": "^0.
|
|
23
|
-
"@kubun/id": "^0.
|
|
24
|
-
"@kubun/plugin-service-api": "^0.
|
|
25
|
-
"@kubun/plugin-service-server": "^0.
|
|
26
|
-
"@kubun/protocol": "^0.
|
|
27
|
-
"@kubun/
|
|
28
|
-
"@
|
|
17
|
+
"@kokuin/token": "^0.5.1",
|
|
18
|
+
"@kubun/credential": "^0.18.0",
|
|
19
|
+
"@kubun/db": "^0.18.0",
|
|
20
|
+
"@kubun/engine": "^0.18.0",
|
|
21
|
+
"@kubun/graphql": "^0.18.0",
|
|
22
|
+
"@kubun/hlc": "^0.18.0",
|
|
23
|
+
"@kubun/id": "^0.18.0",
|
|
24
|
+
"@kubun/plugin-service-api": "^0.18.0",
|
|
25
|
+
"@kubun/plugin-service-server": "^0.18.0",
|
|
26
|
+
"@kubun/protocol": "^0.18.0",
|
|
27
|
+
"@kubun/service-credential-api": "^0.18.0",
|
|
28
|
+
"@kubun/store-credential": "^0.18.0",
|
|
29
|
+
"@sozai/codec": "^0.4.3",
|
|
29
30
|
"@sozai/runtime": "^0.1.0",
|
|
30
31
|
"graphql": "^17.0.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@enkaku/client": "^0.21.
|
|
34
|
-
"@enkaku/transport": "^0.21.
|
|
35
|
-
"@kokuin/capability": "^0.
|
|
34
|
+
"@enkaku/client": "^0.21.5",
|
|
35
|
+
"@enkaku/transport": "^0.21.5",
|
|
36
|
+
"@kokuin/capability": "^0.4.2",
|
|
36
37
|
"@kokuin/controller": "^0.1.0",
|
|
37
|
-
"@kubun/db-better-sqlite": "^0.
|
|
38
|
-
"@kubun/
|
|
39
|
-
"@kubun/store-
|
|
40
|
-
"@kubun/store-graph": "^0.17.1",
|
|
38
|
+
"@kubun/db-better-sqlite": "^0.18.0",
|
|
39
|
+
"@kubun/store-controller": "^0.18.0",
|
|
40
|
+
"@kubun/store-graph": "^0.18.0",
|
|
41
41
|
"@kubun/test-utils": "^0.0.0"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|