@kubun/service-controller-log-api 0.14.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/LICENSE.md ADDED
@@ -0,0 +1,57 @@
1
+ # The Prosperity Public License 3.0.0
2
+
3
+ Contributor: Paul Le Cam
4
+
5
+ Source Code: https://github.com/PaulLeCam/kubun
6
+
7
+ ## Purpose
8
+
9
+ This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
10
+
11
+ ## Agreement
12
+
13
+ In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
14
+
15
+ ## Notices
16
+
17
+ Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
18
+
19
+ ## Commercial Trial
20
+
21
+ Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
22
+
23
+ ## Contributions Back
24
+
25
+ Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
26
+
27
+ ## Personal Uses
28
+
29
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
30
+
31
+ ## Noncommercial Organizations
32
+
33
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
34
+
35
+ ## Defense
36
+
37
+ Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
38
+
39
+ ## Copyright
40
+
41
+ The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
42
+
43
+ ## Patent
44
+
45
+ The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
46
+
47
+ ## Reliability
48
+
49
+ The contributor can't revoke this license.
50
+
51
+ ## Excuse
52
+
53
+ You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
54
+
55
+ ## No Liability
56
+
57
+ ***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
@@ -0,0 +1,4 @@
1
+ import type { Client } from '@enkaku/client';
2
+ import type { ControllerLogSource } from '@kubun/engine';
3
+ import type { ControllerLogServiceProtocol } from './index.js';
4
+ export declare function toControllerLogSource(client: Client<ControllerLogServiceProtocol>): ControllerLogSource;
package/lib/adapter.js ADDED
@@ -0,0 +1,20 @@
1
+ // Runtime adapter: turn an authenticated `Client<ControllerLogServiceProtocol>`
2
+ // into a `controller-log-source` the consuming engine consults. Registered with
3
+ // `transport: 'direct'`, it sits between the resolver's local cache (`none`) and
4
+ // p2p's `networked` provider — so a controller log the serving engine holds
5
+ // resolves over the direct link before any group-peer round trip.
6
+ export function toControllerLogSource(client) {
7
+ return {
8
+ async fetch (did) {
9
+ const log = await client.request('controller/resolve', {
10
+ param: {
11
+ did
12
+ }
13
+ });
14
+ // The wire result is a `SignedEvent[]` (typed as object-array for the
15
+ // schema) or null; a source with no answer yields undefined so the resolver
16
+ // falls through to the next provider.
17
+ return log == null ? undefined : log;
18
+ }
19
+ };
20
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,50 @@
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;
package/lib/index.js ADDED
@@ -0,0 +1,73 @@
1
+ // Cross-plugin contract for the controller-log service: the Enkaku protocol a
2
+ // consuming engine drives over a transport and the controller-log plugin serves
3
+ // as a `service` provider. Alongside the protocol schema it carries the runtime
4
+ // `toControllerLogSource` adapter (see `./adapter.js`), which maps an
5
+ // authenticated client into the `controller-log-source` provider a consuming
6
+ // engine consults, plus the server factory (see `./service.js`).
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
+ };
@@ -0,0 +1,15 @@
1
+ import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
2
+ /**
3
+ * The controller-log service provider. It registers a `service` provider on the
4
+ * hosting engine exposing `controller/resolve` and `controller/ingest` over a
5
+ * transport, so a consuming engine resolves or pushes a `did:kokuin:` key-event
6
+ * log against the hosting engine's authoritative controller store.
7
+ *
8
+ * Neither handler subject-gates. A key-event log is public, verifiable material,
9
+ * 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`.
14
+ */
15
+ export declare function createControllerLogService(): (params: PluginFactoryParams) => KubunPlugin;
package/lib/service.js ADDED
@@ -0,0 +1,88 @@
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';
5
+ /**
6
+ * The controller-log service provider. It registers a `service` provider on the
7
+ * hosting engine exposing `controller/resolve` and `controller/ingest` over a
8
+ * transport, so a consuming engine resolves or pushes a `did:kokuin:` key-event
9
+ * log against the hosting engine's authoritative controller store.
10
+ *
11
+ * Neither handler subject-gates. A key-event log is public, verifiable material,
12
+ * 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`.
17
+ */ export function createControllerLogService() {
18
+ 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
+ const handlers = {
43
+ 'controller/resolve': async (context)=>{
44
+ const param = context.param;
45
+ const log = await (await getControllerStore(params.db)).get(param.did);
46
+ return log ?? null;
47
+ },
48
+ 'controller/ingest': async (context)=>{
49
+ 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
+ });
70
+ }
71
+ };
72
+ params.registerProvider('service', {
73
+ name: 'controller-log',
74
+ handlers,
75
+ accessRules: {
76
+ 'controller/resolve': {
77
+ allow: true
78
+ },
79
+ 'controller/ingest': {
80
+ allow: true
81
+ }
82
+ }
83
+ });
84
+ return {
85
+ name: 'controller-log-service'
86
+ };
87
+ };
88
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@kubun/service-controller-log-api",
3
+ "version": "0.14.0",
4
+ "keywords": [],
5
+ "license": "see LICENSE.md",
6
+ "sideEffects": false,
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./lib/index.js"
10
+ },
11
+ "main": "lib/index.js",
12
+ "types": "lib/index.d.ts",
13
+ "files": [
14
+ "lib/*",
15
+ "LICENSE.md"
16
+ ],
17
+ "dependencies": {
18
+ "@enkaku/client": "^0.21.0",
19
+ "@enkaku/protocol": "^0.21.0",
20
+ "@kokuin/capability": "^0.3.0",
21
+ "@kokuin/controller": "^0.1.0",
22
+ "@kubun/engine": "^0.14.0",
23
+ "@kubun/plugin-service-api": "^0.14.0",
24
+ "@kubun/store-controller": "^0.14.0"
25
+ },
26
+ "devDependencies": {
27
+ "@enkaku/transport": "^0.21.0",
28
+ "@kokuin/token": "^0.5.0",
29
+ "@kubun/db": "^0.14.0",
30
+ "@kubun/db-better-sqlite": "^0.14.0",
31
+ "@kubun/plugin-service-server": "^0.14.0",
32
+ "@kubun/store-graph": "^0.14.0",
33
+ "@kubun/test-utils": "^0.13.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
40
+ "build:clean": "del lib",
41
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
42
+ "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
43
+ "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
44
+ "test": "pnpm run test:types && pnpm run test:unit",
45
+ "test:types": "tsc --noEmit -p tsconfig.test.json",
46
+ "test:unit": "vitest run"
47
+ }
48
+ }