@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 +57 -0
- package/lib/adapter.d.ts +4 -0
- package/lib/adapter.js +20 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.js +73 -0
- package/lib/service.d.ts +15 -0
- package/lib/service.js +88 -0
- package/package.json +48 -0
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.***
|
package/lib/adapter.d.ts
ADDED
|
@@ -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
|
+
};
|
package/lib/service.d.ts
ADDED
|
@@ -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
|
+
}
|