@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 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
  *
@@ -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.13.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.13.0",
33
- "@kubun/test-utils": "^0.13.0",
34
- "@kubun/store-controller": "^0.13.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"