@kubun/plugin-credential 0.17.5 → 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 +149 -24
- 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,7 @@ 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))
|
|
152
235
|
});
|
|
153
236
|
},
|
|
154
237
|
'credential/get-ref': (context)=>{
|
|
@@ -157,7 +240,10 @@ function readAccountPeek(raw) {
|
|
|
157
240
|
context,
|
|
158
241
|
subject: param.ownerDID,
|
|
159
242
|
emptyResult: null,
|
|
160
|
-
proceed: ()=>
|
|
243
|
+
proceed: ()=>localRead(context, null, ()=>{
|
|
244
|
+
requireLocalGrantAddress(param);
|
|
245
|
+
return readSeam.getVersionedCredentialRef(param.providerName, param.ownerDID);
|
|
246
|
+
})
|
|
161
247
|
});
|
|
162
248
|
},
|
|
163
249
|
// The same vault read as `credential/get` — the DB credential API, which
|
|
@@ -169,11 +255,11 @@ function readAccountPeek(raw) {
|
|
|
169
255
|
context,
|
|
170
256
|
subject: param.ownerDID,
|
|
171
257
|
emptyResult: null,
|
|
172
|
-
proceed: async ()=>{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
})
|
|
177
263
|
});
|
|
178
264
|
},
|
|
179
265
|
'credential/provenance': (context)=>{
|
|
@@ -182,7 +268,7 @@ function readAccountPeek(raw) {
|
|
|
182
268
|
context,
|
|
183
269
|
subject: param.ownerDID,
|
|
184
270
|
emptyResult: null,
|
|
185
|
-
proceed: ()=>readSeam.getCredentialProvenance(param.providerName, param.ownerDID)
|
|
271
|
+
proceed: ()=>localRead(context, null, ()=>readSeam.getCredentialProvenance(param.providerName, param.ownerDID))
|
|
186
272
|
});
|
|
187
273
|
},
|
|
188
274
|
'credential/create': async (context)=>{
|
|
@@ -191,6 +277,7 @@ function readAccountPeek(raw) {
|
|
|
191
277
|
context,
|
|
192
278
|
subject: param.ownerDID
|
|
193
279
|
});
|
|
280
|
+
await localWrite(context);
|
|
194
281
|
// `ownerWrappableDID` is the wrappable spelling of the SAME identity the
|
|
195
282
|
// authorizer just cleared (`ownerDID`). Nothing downstream re-checks that,
|
|
196
283
|
// so without this a caller authorized for the owner could wrap the token to
|
|
@@ -202,13 +289,24 @@ function readAccountPeek(raw) {
|
|
|
202
289
|
param.ownerWrappableDID,
|
|
203
290
|
param.controllerWrappableDID
|
|
204
291
|
]);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
310
|
return null;
|
|
213
311
|
},
|
|
214
312
|
'credential/refresh': async (context)=>{
|
|
@@ -217,6 +315,7 @@ function readAccountPeek(raw) {
|
|
|
217
315
|
context,
|
|
218
316
|
subject: param.ownerDID
|
|
219
317
|
});
|
|
318
|
+
await localWrite(context);
|
|
220
319
|
await writeInTx((seam)=>seam.setCredential(param.providerName, param.ownerDID, param.credential));
|
|
221
320
|
return null;
|
|
222
321
|
},
|
|
@@ -226,13 +325,34 @@ function readAccountPeek(raw) {
|
|
|
226
325
|
context,
|
|
227
326
|
subject: param.ownerDID
|
|
228
327
|
});
|
|
328
|
+
await localWrite(context);
|
|
329
|
+
requireLocalGrantAddress(param);
|
|
229
330
|
return writeInTx(async (seam)=>{
|
|
230
331
|
// The authorized alias must still name the entry being refreshed.
|
|
231
|
-
const ref = await seam.
|
|
332
|
+
const ref = await seam.getVersionedCredentialRef(param.providerName, param.ownerDID);
|
|
232
333
|
if (ref?.entryID !== param.entryID) return 'absent';
|
|
233
|
-
return seam.setCredentialForEntry(param)
|
|
334
|
+
return param.expectedVersion == null ? seam.setCredentialForEntry(param) : seam.setCredentialForEntryIfUnchanged({
|
|
335
|
+
...param,
|
|
336
|
+
expectedVersion: param.expectedVersion
|
|
337
|
+
});
|
|
234
338
|
});
|
|
235
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
|
+
},
|
|
236
356
|
'credential/delete': async (context)=>{
|
|
237
357
|
const param = context.param;
|
|
238
358
|
await authorizeWrite({
|
|
@@ -248,6 +368,7 @@ function readAccountPeek(raw) {
|
|
|
248
368
|
context,
|
|
249
369
|
subject: param.ownerDID
|
|
250
370
|
}); // Gate A
|
|
371
|
+
await localWrite(context);
|
|
251
372
|
try {
|
|
252
373
|
await writeInTx((seam)=>seam.bindAlias(param.providerName, param.ownerDID, param.entryID));
|
|
253
374
|
} catch (err) {
|
|
@@ -259,6 +380,7 @@ function readAccountPeek(raw) {
|
|
|
259
380
|
params.registerProvider('service', {
|
|
260
381
|
name: 'credential',
|
|
261
382
|
handlers,
|
|
383
|
+
protocol: credentialServiceProtocol,
|
|
262
384
|
accessRules: {
|
|
263
385
|
'credential/get': {
|
|
264
386
|
allow: true
|
|
@@ -281,6 +403,9 @@ function readAccountPeek(raw) {
|
|
|
281
403
|
'credential/refresh-entry': {
|
|
282
404
|
allow: true
|
|
283
405
|
},
|
|
406
|
+
'credential/adopt-grant': {
|
|
407
|
+
allow: true
|
|
408
|
+
},
|
|
284
409
|
'credential/delete': {
|
|
285
410
|
allow: true
|
|
286
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": {
|