@kubun/service-controller-log-api 0.17.0 → 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/index.d.ts CHANGED
@@ -1,50 +1,3 @@
1
1
  export { toControllerLogSource } from './adapter.js';
2
- export { createControllerLogService } from './service.js';
3
- export declare const controllerLogServiceProtocol: {
4
- readonly 'controller/resolve': {
5
- readonly type: "request";
6
- readonly param: {
7
- readonly type: "object";
8
- readonly properties: {
9
- readonly did: {
10
- readonly type: "string";
11
- };
12
- };
13
- readonly required: readonly ["did"];
14
- readonly additionalProperties: false;
15
- };
16
- readonly result: {
17
- readonly anyOf: [{
18
- readonly type: "array";
19
- readonly items: {
20
- readonly type: "object";
21
- };
22
- }, {
23
- readonly type: "null";
24
- }];
25
- };
26
- };
27
- readonly 'controller/ingest': {
28
- readonly type: "request";
29
- readonly param: {
30
- readonly type: "object";
31
- readonly properties: {
32
- readonly did: {
33
- readonly type: "string";
34
- };
35
- readonly log: {
36
- readonly type: "array";
37
- readonly items: {
38
- readonly type: "object";
39
- };
40
- };
41
- };
42
- readonly required: readonly ["did", "log"];
43
- readonly additionalProperties: false;
44
- };
45
- readonly result: {
46
- readonly type: "null";
47
- };
48
- };
49
- };
50
- export type ControllerLogServiceProtocol = typeof controllerLogServiceProtocol;
2
+ export { type ControllerLogServiceProtocol, controllerLogServiceProtocol, } from './protocol.js';
3
+ export { ControllerLogIngestRefusedError, createControllerLogService } from './service.js';
package/lib/index.js CHANGED
@@ -5,69 +5,5 @@
5
5
  // authenticated client into the `controller-log-source` provider a consuming
6
6
  // engine consults, plus the server factory (see `./service.js`).
7
7
  export { toControllerLogSource } from './adapter.js';
8
- export { createControllerLogService } from './service.js';
9
- // `did` is a request PARAM, not the Enkaku message `sub`: a caller resolving or
10
- // ingesting another controller's log is not acting as that controller, and the
11
- // host rejects a `sub` that differs from `iss`. Same convention as
12
- // `service-credential-api`'s `ownerDID`.
13
- //
14
- // `resolve` yields the raw key-event log (a `SignedEvent[]`) or null; the log has
15
- // no JSON-schema here, so an array-of-object result is enough for the wire and the
16
- // adapter casts. `ingest` acks with null; its `log` is folded through kokuin's
17
- // anti-truncation guard server-side, so a truncated or forked push is refused
18
- // there rather than trusted.
19
- export const controllerLogServiceProtocol = {
20
- 'controller/resolve': {
21
- type: 'request',
22
- param: {
23
- type: 'object',
24
- properties: {
25
- did: {
26
- type: 'string'
27
- }
28
- },
29
- required: [
30
- 'did'
31
- ],
32
- additionalProperties: false
33
- },
34
- result: {
35
- anyOf: [
36
- {
37
- type: 'array',
38
- items: {
39
- type: 'object'
40
- }
41
- },
42
- {
43
- type: 'null'
44
- }
45
- ]
46
- }
47
- },
48
- 'controller/ingest': {
49
- type: 'request',
50
- param: {
51
- type: 'object',
52
- properties: {
53
- did: {
54
- type: 'string'
55
- },
56
- log: {
57
- type: 'array',
58
- items: {
59
- type: 'object'
60
- }
61
- }
62
- },
63
- required: [
64
- 'did',
65
- 'log'
66
- ],
67
- additionalProperties: false
68
- },
69
- result: {
70
- type: 'null'
71
- }
72
- }
73
- };
8
+ export { controllerLogServiceProtocol } from './protocol.js';
9
+ export { ControllerLogIngestRefusedError, createControllerLogService } from './service.js';
@@ -0,0 +1,48 @@
1
+ export declare const controllerLogServiceProtocol: {
2
+ readonly 'controller/resolve': {
3
+ readonly type: "request";
4
+ readonly param: {
5
+ readonly type: "object";
6
+ readonly properties: {
7
+ readonly did: {
8
+ readonly type: "string";
9
+ };
10
+ };
11
+ readonly required: readonly ["did"];
12
+ readonly additionalProperties: false;
13
+ };
14
+ readonly result: {
15
+ readonly anyOf: [{
16
+ readonly type: "array";
17
+ readonly items: {
18
+ readonly type: "object";
19
+ };
20
+ }, {
21
+ readonly type: "null";
22
+ }];
23
+ };
24
+ };
25
+ readonly 'controller/ingest': {
26
+ readonly type: "request";
27
+ readonly param: {
28
+ readonly type: "object";
29
+ readonly properties: {
30
+ readonly did: {
31
+ readonly type: "string";
32
+ };
33
+ readonly log: {
34
+ readonly type: "array";
35
+ readonly items: {
36
+ readonly type: "object";
37
+ };
38
+ };
39
+ };
40
+ readonly required: readonly ["did", "log"];
41
+ readonly additionalProperties: false;
42
+ };
43
+ readonly result: {
44
+ readonly type: "null";
45
+ };
46
+ };
47
+ };
48
+ export type ControllerLogServiceProtocol = typeof controllerLogServiceProtocol;
@@ -0,0 +1,65 @@
1
+ // `did` is a request PARAM, not the Enkaku message `sub`: a caller resolving or
2
+ // ingesting another controller's log is not acting as that controller, and the
3
+ // host rejects a `sub` that differs from `iss`. Same convention as
4
+ // `service-credential-api`'s `ownerDID`.
5
+ //
6
+ // `resolve` yields the raw key-event log (a `SignedEvent[]`) or null; the log has
7
+ // no JSON-schema here, so an array-of-object result is enough for the wire and the
8
+ // adapter casts. `ingest` acks with null; its `log` is folded through kokuin's
9
+ // anti-truncation guard server-side, so a truncated or forked push is refused
10
+ // there rather than trusted.
11
+ export const controllerLogServiceProtocol = {
12
+ 'controller/resolve': {
13
+ type: 'request',
14
+ param: {
15
+ type: 'object',
16
+ properties: {
17
+ did: {
18
+ type: 'string'
19
+ }
20
+ },
21
+ required: [
22
+ 'did'
23
+ ],
24
+ additionalProperties: false
25
+ },
26
+ result: {
27
+ anyOf: [
28
+ {
29
+ type: 'array',
30
+ items: {
31
+ type: 'object'
32
+ }
33
+ },
34
+ {
35
+ type: 'null'
36
+ }
37
+ ]
38
+ }
39
+ },
40
+ 'controller/ingest': {
41
+ type: 'request',
42
+ param: {
43
+ type: 'object',
44
+ properties: {
45
+ did: {
46
+ type: 'string'
47
+ },
48
+ log: {
49
+ type: 'array',
50
+ items: {
51
+ type: 'object'
52
+ }
53
+ }
54
+ },
55
+ required: [
56
+ 'did',
57
+ 'log'
58
+ ],
59
+ additionalProperties: false
60
+ },
61
+ result: {
62
+ type: 'null'
63
+ }
64
+ }
65
+ };
package/lib/service.d.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
1
+ import type { IngestOutcome, KubunPlugin, PluginFactoryParams } from '@kubun/engine';
2
+ /** A pushed log that did not become, or was not already, the authoritative head. */
3
+ export declare class ControllerLogIngestRefusedError extends Error {
4
+ #private;
5
+ constructor(did: string, outcome: IngestOutcome);
6
+ get outcome(): IngestOutcome;
7
+ }
2
8
  /**
3
9
  * The controller-log service provider. It registers a `service` provider on the
4
10
  * hosting engine exposing `controller/resolve` and `controller/ingest` over a
@@ -7,9 +13,9 @@ import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
7
13
  *
8
14
  * Neither handler subject-gates. A key-event log is public, verifiable material,
9
15
  * not owner-private data: `resolve` must answer for any DID (resolving another
10
- * user's controller is normal), and `ingest` is protected by kokuin's fold
11
- * guard — not a subject authorizer — so any verified caller may push a log the
12
- * guard then arbitrates. The transport access rules therefore grant both to every
13
- * verified issuer, and no handler calls `context.authorize`.
16
+ * user's controller is normal), and `ingest` is protected by the engine's
17
+ * controller head arbitration — not a subject authorizer — so any verified
18
+ * caller may push a log it then arbitrates. The transport access rules therefore
19
+ * grant both to every verified issuer, and no handler calls `context.authorize`.
14
20
  */
15
21
  export declare function createControllerLogService(): (params: PluginFactoryParams) => KubunPlugin;
package/lib/service.js CHANGED
@@ -1,7 +1,23 @@
1
- import { createControllerCapabilityVerifier } from '@kokuin/capability';
2
- import { createControllerResolver } from '@kokuin/controller';
3
- import { normalizeDID } from '@kokuin/token';
4
- import { getControllerStore } from '@kubun/store-controller';
1
+ import { digestOf } from '@kokuin/controller';
2
+ import { controllerLogServiceProtocol } from './protocol.js';
3
+ /** A pushed log that did not become, or was not already, the authoritative head. */ export class ControllerLogIngestRefusedError extends Error {
4
+ #outcome;
5
+ constructor(did, outcome){
6
+ super(`Controller log not accepted (${outcome}): ${did}`);
7
+ this.name = 'ControllerLogIngestRefusedError';
8
+ this.#outcome = outcome;
9
+ }
10
+ get outcome() {
11
+ return this.#outcome;
12
+ }
13
+ }
14
+ function logDigest(log) {
15
+ try {
16
+ return digestOf(log);
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
5
21
  /**
6
22
  * The controller-log service provider. It registers a `service` provider on the
7
23
  * hosting engine exposing `controller/resolve` and `controller/ingest` over a
@@ -10,63 +26,34 @@ import { getControllerStore } from '@kubun/store-controller';
10
26
  *
11
27
  * Neither handler subject-gates. A key-event log is public, verifiable material,
12
28
  * not owner-private data: `resolve` must answer for any DID (resolving another
13
- * user's controller is normal), and `ingest` is protected by kokuin's fold
14
- * guard — not a subject authorizer — so any verified caller may push a log the
15
- * guard then arbitrates. The transport access rules therefore grant both to every
16
- * verified issuer, and no handler calls `context.authorize`.
29
+ * user's controller is normal), and `ingest` is protected by the engine's
30
+ * controller head arbitration — not a subject authorizer — so any verified
31
+ * caller may push a log it then arbitrates. The transport access rules therefore
32
+ * grant both to every verified issuer, and no handler calls `context.authorize`.
17
33
  */ export function createControllerLogService() {
18
34
  return (params)=>{
19
- // Per-DID single-flight: the ingest fold reads the stored head then writes,
20
- // and the store's `set` is unconditional LWW. Two concurrent ingests of
21
- // conflicting valid branches would both read the same head and both write —
22
- // last-writer-wins accepts a fork with no `LOG_FORKED` and drops an accepted
23
- // event. Chaining each DID's fold+write on the prior one makes the second read
24
- // the first's committed head, so its conflicting branch folds as a fork.
25
- const ingestChains = new Map();
26
- const ingestSerialized = (did, run)=>{
27
- const key = normalizeDID(did);
28
- const tail = ingestChains.get(key);
29
- // `.then(run, run)` so a rejected predecessor still lets the next run rather
30
- // than wedging the whole chain.
31
- const next = tail == null ? run() : tail.then(run, run);
32
- ingestChains.set(key, next);
33
- // Cleanup observes `next` on a separate branch that always resolves, so a
34
- // losing branch's rejection is not re-propagated by an unawaited derived
35
- // promise. The caller still awaits `next` and sees the rejection.
36
- const cleanup = ()=>{
37
- if (ingestChains.get(key) === next) ingestChains.delete(key);
38
- };
39
- next.then(cleanup, cleanup);
40
- return next;
41
- };
42
35
  const handlers = {
43
36
  'controller/resolve': async (context)=>{
44
37
  const param = context.param;
45
- const log = await (await getControllerStore(params.db)).get(param.did);
46
- return log ?? null;
38
+ // Only a checked head is handed out: a forked DID has no log to answer with.
39
+ const head = await params.controllerHead(params.db, param.did);
40
+ return head.status === 'ok' ? head.log : null;
47
41
  },
48
42
  'controller/ingest': async (context)=>{
49
43
  const param = context.param;
50
- return ingestSerialized(param.did, async ()=>{
51
- const store = await getControllerStore(params.db);
52
- // Fold the untrusted incoming log through the same anti-truncation guard
53
- // the engine's controller resolver uses (`authoritativeStates` + a
54
- // write-through `history`), rather than a raw `store.set` that would bypass
55
- // it. A behind/truncated log loses to the stored head, a fork throws
56
- // `LOG_FORKED`, and a valid extension write-throughs — all owned by kokuin.
57
- const resolver = createControllerResolver({
58
- loadLog: async (did)=>did === param.did ? param.log : undefined,
59
- verifyCapability: createControllerCapabilityVerifier(),
60
- history: {
61
- get: (did)=>store.get(did),
62
- set: (did, log)=>store.set(did, log)
63
- }
64
- });
65
- // Resolving triggers the fold and its `history.set` write-through; the
66
- // resolved key is discarded — the store write is the effect we want.
67
- await resolver.resolve(param.did, {});
68
- return null;
69
- });
44
+ // On the base provider in its own transaction, so a fork it records
45
+ // survives whatever the caller does next.
46
+ const outcome = await params.ingestControllerLog(params.db, param.did, [
47
+ param.log
48
+ ], 'service');
49
+ if (outcome === 'advanced' || outcome === 'fork-cleared') return null;
50
+ if (outcome === 'unchanged') {
51
+ // An equal replay is accepted; a behind or losing log is refused.
52
+ const head = await params.controllerHead(params.db, param.did);
53
+ const pushed = logDigest(param.log);
54
+ if (head.status === 'ok' && pushed != null && logDigest(head.log) === pushed) return null;
55
+ }
56
+ throw new ControllerLogIngestRefusedError(param.did, outcome);
70
57
  }
71
58
  };
72
59
  params.registerProvider('service', {
@@ -79,7 +66,8 @@ import { getControllerStore } from '@kubun/store-controller';
79
66
  'controller/ingest': {
80
67
  allow: true
81
68
  }
82
- }
69
+ },
70
+ protocol: controllerLogServiceProtocol
83
71
  });
84
72
  return {
85
73
  name: 'controller-log-service'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/service-controller-log-api",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,21 +15,21 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "@enkaku/client": "^0.21.2",
19
- "@enkaku/protocol": "^0.21.1",
20
- "@kokuin/capability": "^0.3.0",
18
+ "@enkaku/client": "^0.21.5",
19
+ "@enkaku/protocol": "^0.21.5",
20
+ "@kokuin/capability": "^0.4.2",
21
21
  "@kokuin/controller": "^0.1.0",
22
- "@kubun/engine": "^0.17.0",
23
- "@kubun/plugin-service-api": "^0.17.0",
24
- "@kubun/store-controller": "^0.17.0"
22
+ "@kubun/engine": "^0.18.0",
23
+ "@kubun/plugin-service-api": "^0.18.0",
24
+ "@kubun/store-controller": "^0.18.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@enkaku/transport": "^0.21.0",
28
- "@kokuin/token": "^0.5.0",
29
- "@kubun/db": "^0.17.0",
30
- "@kubun/db-better-sqlite": "^0.17.0",
31
- "@kubun/plugin-service-server": "^0.17.0",
32
- "@kubun/store-graph": "^0.17.0",
27
+ "@enkaku/transport": "^0.21.5",
28
+ "@kokuin/token": "^0.5.1",
29
+ "@kubun/db": "^0.18.0",
30
+ "@kubun/db-better-sqlite": "^0.18.0",
31
+ "@kubun/plugin-service-server": "^0.18.0",
32
+ "@kubun/store-graph": "^0.18.0",
33
33
  "@kubun/test-utils": "^0.0.0"
34
34
  },
35
35
  "publishConfig": {