@kubun/service-controller-log-api 0.16.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 +2 -49
- package/lib/index.js +2 -66
- package/lib/protocol.d.ts +48 -0
- package/lib/protocol.js +65 -0
- package/lib/service.d.ts +11 -5
- package/lib/service.js +42 -54
- package/package.json +15 -15
package/lib/index.d.ts
CHANGED
|
@@ -1,50 +1,3 @@
|
|
|
1
1
|
export { toControllerLogSource } from './adapter.js';
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
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 {
|
|
9
|
-
|
|
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;
|
package/lib/protocol.js
ADDED
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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.
|
|
19
|
-
"@enkaku/protocol": "^0.21.
|
|
20
|
-
"@kokuin/capability": "^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.
|
|
23
|
-
"@kubun/plugin-service-api": "^0.
|
|
24
|
-
"@kubun/store-controller": "^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.
|
|
28
|
-
"@kokuin/token": "^0.5.
|
|
29
|
-
"@kubun/db": "^0.
|
|
30
|
-
"@kubun/db-better-sqlite": "^0.
|
|
31
|
-
"@kubun/plugin-service-server": "^0.
|
|
32
|
-
"@kubun/store-graph": "^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": {
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
40
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",
|
|
41
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
42
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
43
43
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
44
44
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
45
45
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|