@kubun/plugin-credential 0.13.0 → 0.15.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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/service.d.ts +16 -0
- package/lib/service.js +176 -0
- package/package.json +18 -12
package/lib/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { CredentialAuthority, CredentialAuthorityParams, CredentialAuthoriz
|
|
|
3
3
|
export type { CredentialEntryResult, CredentialKeyResult, CredentialQueryContext, CredentialWrappingResult, PutCredentialEntryInput, PutCredentialKeyInput, PutCredentialWrappingInput, } from './context.js';
|
|
4
4
|
export { createCredentialQueryContext } from './context.js';
|
|
5
5
|
export { createCredentialSchemaExtension } from './schema.js';
|
|
6
|
+
export { createCredentialServicePlugin } from './service.js';
|
|
6
7
|
/**
|
|
7
8
|
* No `api`, so nothing reaches this plugin through `engine.getAPI('credential')`.
|
|
8
9
|
*
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createCredentialQueryContext } from './context.js';
|
|
|
3
3
|
import { createCredentialSchemaExtension } from './schema.js';
|
|
4
4
|
export { createCredentialQueryContext } from './context.js';
|
|
5
5
|
export { createCredentialSchemaExtension } from './schema.js';
|
|
6
|
+
export { createCredentialServicePlugin } from './service.js';
|
|
6
7
|
/**
|
|
7
8
|
* No `api`, so nothing reaches this plugin through `engine.getAPI('credential')`.
|
|
8
9
|
*
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
|
|
2
|
+
/**
|
|
3
|
+
* The credential service provider. Unlike {@link createCredentialPlugin}
|
|
4
|
+
* (graph-schema-only, for an engine that consumes credentials), this registers a
|
|
5
|
+
* `service` provider that hosts the vault: it builds the credential seam over
|
|
6
|
+
* this engine's own db/HLC/identity and exposes
|
|
7
|
+
* `credential/get|create|refresh|delete|provenance` over a transport. The mint
|
|
8
|
+
* runs here, resolving a `did:kokuin:` owner-wrapping locally because this engine
|
|
9
|
+
* holds the owner's controller log.
|
|
10
|
+
*
|
|
11
|
+
* Each handler rides the per-serve authorizer through its call context: reads
|
|
12
|
+
* return the empty result on denial (no existence oracle), writes throw the
|
|
13
|
+
* host's `KB07`. The transport access rules grant every procedure to any verified
|
|
14
|
+
* issuer, so the subject-authorizer — not the rule map — is the gate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createCredentialServicePlugin(): (params: PluginFactoryParams) => KubunPlugin;
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { isSigningIdentity, normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { createCredentialAPI, createCredentialManager } from '@kubun/credential';
|
|
3
|
+
import { authorizeRead, authorizeWrite, ServiceAuthorizationError } from '@kubun/plugin-service-server';
|
|
4
|
+
import { credentialStoreDefinition, getCredentialStore } from '@kubun/store-credential';
|
|
5
|
+
// The spelling of a DID that can be encrypted to. A did:peer:4 identity carries a
|
|
6
|
+
// longForm that resolves the agreement key; its short id resolves none. For
|
|
7
|
+
// did:key the two forms are identical.
|
|
8
|
+
function wrappableDID(identity) {
|
|
9
|
+
const longForm = identity.longForm;
|
|
10
|
+
return typeof longForm === 'string' ? longForm : identity.id;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The credential service provider. Unlike {@link createCredentialPlugin}
|
|
14
|
+
* (graph-schema-only, for an engine that consumes credentials), this registers a
|
|
15
|
+
* `service` provider that hosts the vault: it builds the credential seam over
|
|
16
|
+
* this engine's own db/HLC/identity and exposes
|
|
17
|
+
* `credential/get|create|refresh|delete|provenance` over a transport. The mint
|
|
18
|
+
* runs here, resolving a `did:kokuin:` owner-wrapping locally because this engine
|
|
19
|
+
* holds the owner's controller log.
|
|
20
|
+
*
|
|
21
|
+
* Each handler rides the per-serve authorizer through its call context: reads
|
|
22
|
+
* return the empty result on denial (no existence oracle), writes throw the
|
|
23
|
+
* host's `KB07`. The transport access rules grant every procedure to any verified
|
|
24
|
+
* issuer, so the subject-authorizer — not the rule map — is the gate.
|
|
25
|
+
*/ export function createCredentialServicePlugin() {
|
|
26
|
+
return (params)=>{
|
|
27
|
+
params.db.register(credentialStoreDefinition);
|
|
28
|
+
// Refused at construction, not at the first mint: every credential row this
|
|
29
|
+
// service writes carries an op signed by this identity, so a non-signing
|
|
30
|
+
// engine identity would fail on the first create instead of at install.
|
|
31
|
+
if (!isSigningIdentity(params.identity)) {
|
|
32
|
+
throw new Error('The credential service plugin requires a SigningIdentity');
|
|
33
|
+
}
|
|
34
|
+
const identity = params.identity;
|
|
35
|
+
const logger = params.getLogger('credential-service');
|
|
36
|
+
const getCredentialManager = async (stores)=>createCredentialManager({
|
|
37
|
+
store: await getCredentialStore(stores),
|
|
38
|
+
identity,
|
|
39
|
+
runtime: params.runtime,
|
|
40
|
+
hlc: params.hlc
|
|
41
|
+
});
|
|
42
|
+
// The mint folds inside its own write tx (`writeInTx`), so its resolver is
|
|
43
|
+
// local-only: a recipient miss must fail closed, never pull a controller log
|
|
44
|
+
// through `consultProviders` while the write lock is held. The pre-warm below
|
|
45
|
+
// resolves first-time recipients with the pull-capable default before the tx.
|
|
46
|
+
const getControllerMethods = (stores)=>[
|
|
47
|
+
params.controllerResolverFor(stores, {
|
|
48
|
+
localOnly: true
|
|
49
|
+
})
|
|
50
|
+
];
|
|
51
|
+
// The seam is bound to whatever provider it is built over: reads run against
|
|
52
|
+
// the root, writes against a transaction (below). A manager, alias store and
|
|
53
|
+
// controller resolver all read from that same provider.
|
|
54
|
+
const makeSeam = (stores)=>createCredentialAPI({
|
|
55
|
+
stores,
|
|
56
|
+
logger,
|
|
57
|
+
getCredentialManager,
|
|
58
|
+
serverWrappableDID: wrappableDID(params.identity),
|
|
59
|
+
getControllerMethods
|
|
60
|
+
});
|
|
61
|
+
const readSeam = makeSeam(params.db);
|
|
62
|
+
// A mint/refresh/delete is multi-statement (key + wrappings + entry + alias
|
|
63
|
+
// pointer). Outside a graph mutation there is no ambient transaction, so run
|
|
64
|
+
// each write in its own: a mid-mint failure would otherwise leave an
|
|
65
|
+
// orphan key with no alias pointer in the vault.
|
|
66
|
+
const writeInTx = (fn)=>params.db.withTransaction((tx)=>fn(makeSeam(tx)));
|
|
67
|
+
// A first-time `did:kokuin:` owner (or recovery controller) may hold no log in
|
|
68
|
+
// this engine's controller store yet. Resolve it through the pull-allowed ROOT
|
|
69
|
+
// resolver before the mint tx opens: on a store miss this pulls via the
|
|
70
|
+
// `controller-log-source` providers and write-throughs the folded log, so the
|
|
71
|
+
// in-tx mint's `loadLog` then reads it locally without pulling inside the tx.
|
|
72
|
+
// `did:key` / `did:peer:4` recipients carry their own agreement key and never
|
|
73
|
+
// reach the controller resolver, so they are skipped. A genuinely unresolvable
|
|
74
|
+
// recipient throws here, before any write — the mint tx never opens.
|
|
75
|
+
const prewarmWrappingRecipients = async (recipients)=>{
|
|
76
|
+
const resolver = params.controllerResolverFor(params.db);
|
|
77
|
+
const prefix = `did:${resolver.method}:`;
|
|
78
|
+
for (const recipient of recipients){
|
|
79
|
+
if (recipient?.startsWith(prefix)) {
|
|
80
|
+
await resolver.resolve(recipient, {});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// `ownerDID` is the authorization subject (see the protocol contract), NOT the
|
|
85
|
+
// Enkaku message `sub`. Reads pass it to authorizeRead (empty on denial);
|
|
86
|
+
// writes to authorizeWrite (KB07 on denial).
|
|
87
|
+
const handlers = {
|
|
88
|
+
'credential/get': (context)=>{
|
|
89
|
+
const param = context.param;
|
|
90
|
+
return authorizeRead({
|
|
91
|
+
context,
|
|
92
|
+
subject: param.ownerDID,
|
|
93
|
+
emptyResult: null,
|
|
94
|
+
proceed: ()=>readSeam.getCredential(param.providerName, param.ownerDID)
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
'credential/provenance': (context)=>{
|
|
98
|
+
const param = context.param;
|
|
99
|
+
return authorizeRead({
|
|
100
|
+
context,
|
|
101
|
+
subject: param.ownerDID,
|
|
102
|
+
emptyResult: null,
|
|
103
|
+
proceed: ()=>readSeam.getCredentialProvenance(param.providerName, param.ownerDID)
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
'credential/create': async (context)=>{
|
|
107
|
+
const param = context.param;
|
|
108
|
+
await authorizeWrite({
|
|
109
|
+
context,
|
|
110
|
+
subject: param.ownerDID
|
|
111
|
+
});
|
|
112
|
+
// `ownerWrappableDID` is the wrappable spelling of the SAME identity the
|
|
113
|
+
// authorizer just cleared (`ownerDID`). Nothing downstream re-checks that,
|
|
114
|
+
// so without this a caller authorized for the owner could wrap the token to
|
|
115
|
+
// an unrelated recipient. Same denial path as the authorizer (KB07).
|
|
116
|
+
if (normalizeDID(param.ownerWrappableDID) !== normalizeDID(param.ownerDID)) {
|
|
117
|
+
throw new ServiceAuthorizationError('ownerWrappableDID is not the owner identity');
|
|
118
|
+
}
|
|
119
|
+
await prewarmWrappingRecipients([
|
|
120
|
+
param.ownerWrappableDID,
|
|
121
|
+
param.controllerWrappableDID
|
|
122
|
+
]);
|
|
123
|
+
await writeInTx((seam)=>seam.createCredential({
|
|
124
|
+
providerName: param.providerName,
|
|
125
|
+
ownerDID: param.ownerDID,
|
|
126
|
+
ownerWrappableDID: param.ownerWrappableDID,
|
|
127
|
+
controllerWrappableDID: param.controllerWrappableDID,
|
|
128
|
+
credential: param.credential
|
|
129
|
+
}));
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
'credential/refresh': async (context)=>{
|
|
133
|
+
const param = context.param;
|
|
134
|
+
await authorizeWrite({
|
|
135
|
+
context,
|
|
136
|
+
subject: param.ownerDID
|
|
137
|
+
});
|
|
138
|
+
await writeInTx((seam)=>seam.setCredential(param.providerName, param.ownerDID, param.credential));
|
|
139
|
+
return null;
|
|
140
|
+
},
|
|
141
|
+
'credential/delete': async (context)=>{
|
|
142
|
+
const param = context.param;
|
|
143
|
+
await authorizeWrite({
|
|
144
|
+
context,
|
|
145
|
+
subject: param.ownerDID
|
|
146
|
+
});
|
|
147
|
+
await writeInTx((seam)=>seam.deleteCredential(param.providerName, param.ownerDID));
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
params.registerProvider('service', {
|
|
152
|
+
name: 'credential',
|
|
153
|
+
handlers,
|
|
154
|
+
accessRules: {
|
|
155
|
+
'credential/get': {
|
|
156
|
+
allow: true
|
|
157
|
+
},
|
|
158
|
+
'credential/provenance': {
|
|
159
|
+
allow: true
|
|
160
|
+
},
|
|
161
|
+
'credential/create': {
|
|
162
|
+
allow: true
|
|
163
|
+
},
|
|
164
|
+
'credential/refresh': {
|
|
165
|
+
allow: true
|
|
166
|
+
},
|
|
167
|
+
'credential/delete': {
|
|
168
|
+
allow: true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
name: 'credential-service'
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-credential",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -15,23 +15,29 @@
|
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@kokuin/token": "^0.5.0",
|
|
18
|
+
"@kubun/credential": "^0.15.0",
|
|
19
|
+
"@kubun/db": "^0.15.0",
|
|
20
|
+
"@kubun/engine": "^0.15.0",
|
|
21
|
+
"@kubun/graphql": "^0.15.0",
|
|
22
|
+
"@kubun/hlc": "^0.15.0",
|
|
23
|
+
"@kubun/id": "^0.15.0",
|
|
24
|
+
"@kubun/plugin-service-api": "^0.15.0",
|
|
25
|
+
"@kubun/plugin-service-server": "^0.15.0",
|
|
26
|
+
"@kubun/store-credential": "^0.15.0",
|
|
18
27
|
"@sozai/codec": "^0.4.0",
|
|
19
28
|
"@sozai/runtime": "^0.1.0",
|
|
20
|
-
"graphql": "^16.14.2"
|
|
21
|
-
"@kubun/credential": "^0.13.0",
|
|
22
|
-
"@kubun/hlc": "^0.13.0",
|
|
23
|
-
"@kubun/db": "^0.13.0",
|
|
24
|
-
"@kubun/graphql": "^0.13.0",
|
|
25
|
-
"@kubun/id": "^0.13.0",
|
|
26
|
-
"@kubun/store-credential": "^0.13.0",
|
|
27
|
-
"@kubun/engine": "^0.13.0"
|
|
29
|
+
"graphql": "^16.14.2"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
32
|
+
"@enkaku/client": "^0.21.1",
|
|
33
|
+
"@enkaku/transport": "^0.21.0",
|
|
30
34
|
"@kokuin/capability": "^0.3.0",
|
|
31
35
|
"@kokuin/controller": "^0.1.0",
|
|
32
|
-
"@kubun/db-better-sqlite": "^0.
|
|
33
|
-
"@kubun/
|
|
34
|
-
"@kubun/store-controller": "^0.
|
|
36
|
+
"@kubun/db-better-sqlite": "^0.15.0",
|
|
37
|
+
"@kubun/service-credential-api": "^0.15.0",
|
|
38
|
+
"@kubun/store-controller": "^0.15.0",
|
|
39
|
+
"@kubun/store-graph": "^0.15.0",
|
|
40
|
+
"@kubun/test-utils": "^0.13.0"
|
|
35
41
|
},
|
|
36
42
|
"publishConfig": {
|
|
37
43
|
"access": "public"
|