@serfab/cadre-core 1.16.0 → 1.17.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/README.md +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +11 -2
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +652 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +886 -789
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner keys as an app lists, adds and removes them (`CadreNode.listOwnerKeys`,
|
|
3
|
+
* `CadreNode.addOwner`, `CadreNode.planOwnerKeyRemoval`, `CadreNode.removeOwner`): the
|
|
4
|
+
* projection of the live `OwnerKey` rows an owner-key screen shows, the read-only plan behind a
|
|
5
|
+
* removal's confirmation step, the check that a requested re-vouch stays inside that plan, and
|
|
6
|
+
* the by-name refusals. Pure; the reads and writes themselves are `CadreNode`'s and
|
|
7
|
+
* `ControlDatabase`'s.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CadreOwnerKey,
|
|
12
|
+
CadrePeerRow,
|
|
13
|
+
OwnerGrantStatus,
|
|
14
|
+
OwnerKeyAddRefusal,
|
|
15
|
+
OwnerKeyDependents,
|
|
16
|
+
OwnerKeyRemovalPlan,
|
|
17
|
+
OwnerKeyRemovalRefusal,
|
|
18
|
+
OwnerKeyRow,
|
|
19
|
+
RemoveOwnerOptions,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `CadreNode.addOwner` refused by name, before any write. `code` says which rule
|
|
24
|
+
* ({@link OwnerKeyAddRefusal}), for an app to switch on; the message is for a log.
|
|
25
|
+
*/
|
|
26
|
+
export class OwnerKeyAddRefusedError extends Error {
|
|
27
|
+
readonly code: OwnerKeyAddRefusal;
|
|
28
|
+
readonly key: string;
|
|
29
|
+
|
|
30
|
+
constructor(key: string, code: OwnerKeyAddRefusal) {
|
|
31
|
+
super(`Cannot add owner key ${key}: ${OWNER_KEY_ADD_REFUSAL_REASON[code]}`);
|
|
32
|
+
this.name = 'OwnerKeyAddRefusedError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.key = key;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const OWNER_KEY_ADD_REFUSAL_REASON: Record<OwnerKeyAddRefusal, string> = {
|
|
39
|
+
'alone': 'this node has no connection to the rest of the cadre, and an owner key added now would reach no other machine; try again once connected',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `CadreNode.removeOwner` refused by name, before any write. `code` says which rule
|
|
44
|
+
* ({@link OwnerKeyRemovalRefusal}), for an app to switch on; the message is for a log.
|
|
45
|
+
*/
|
|
46
|
+
export class OwnerKeyRemovalRefusedError extends Error {
|
|
47
|
+
readonly code: OwnerKeyRemovalRefusal;
|
|
48
|
+
readonly key: string;
|
|
49
|
+
|
|
50
|
+
constructor(key: string, code: OwnerKeyRemovalRefusal) {
|
|
51
|
+
super(`Cannot remove owner key ${key}: ${OWNER_KEY_REMOVAL_REFUSAL_REASON[code]}`);
|
|
52
|
+
this.name = 'OwnerKeyRemovalRefusedError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.key = key;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const OWNER_KEY_REMOVAL_REFUSAL_REASON: Record<OwnerKeyRemovalRefusal, string> = {
|
|
59
|
+
'not-an-owner': 'this node holds no enrolled owner key',
|
|
60
|
+
'unknown': 'it is not a live owner key here',
|
|
61
|
+
'last-owner': 'it is the only live owner key',
|
|
62
|
+
'self': 'it is the owner key of this node, and an owner key cannot sign its own removal; ask another owner key',
|
|
63
|
+
'alone': 'this node has no connection to the rest of the cadre, and an owner key re-vouched now would reach no other machine; try again once connected',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** An owner key as an approval prompt names it: `"label" (key)`, or the key alone when it has no label. */
|
|
67
|
+
export function namedOwnerKey(key: string, label: string | null): string {
|
|
68
|
+
return label === null ? key : `"${label}" (${key})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One {@link CadreOwnerKey} per live row, in the rows' order. `heldKey` is the key of this
|
|
73
|
+
* node's owner signer, if one is wired; `anchor` is this node's trusted-owner anchor.
|
|
74
|
+
*/
|
|
75
|
+
export function describeOwnerKeys(rows: readonly OwnerKeyRow[], heldKey: string | null, anchor: ReadonlySet<string>): CadreOwnerKey[] {
|
|
76
|
+
return rows.map((row) => ({
|
|
77
|
+
key: row.key,
|
|
78
|
+
label: row.label,
|
|
79
|
+
vouchedBy: row.vouchOwner,
|
|
80
|
+
heldHere: row.key === heldKey,
|
|
81
|
+
trusted: anchor.has(row.key),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What {@link planOwnerKeyRemoval} judges from: one node's live rows and its own standing. */
|
|
86
|
+
export interface OwnerKeyRemovalPlanInput {
|
|
87
|
+
key: string;
|
|
88
|
+
/** Live `CadrePeer` rows (`ControlDatabase.queryCadrePeers`). */
|
|
89
|
+
rows: readonly CadrePeerRow[];
|
|
90
|
+
/** Live `OwnerKey` rows (`ControlDatabase.queryOwnerKeyRows`). */
|
|
91
|
+
owners: readonly OwnerKeyRow[];
|
|
92
|
+
/** Every owner grant with its standing (`ControlDatabase.listOwnerGrantStatuses`); the live ones the key issued are the plan's `grants`. */
|
|
93
|
+
grants: readonly OwnerGrantStatus[];
|
|
94
|
+
selfPeerId: string | null;
|
|
95
|
+
/** The key of this node's owner signer when it is an enrolled owner (`CadreNode.ownerSignerEnrolledIn`), or null when it cannot sign owner writes. */
|
|
96
|
+
selfOwnerKey: string | null;
|
|
97
|
+
/** This node's trusted-owner anchor, for the listed entry's `trusted`. */
|
|
98
|
+
anchor: ReadonlySet<string>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The plan for removing owner key `key` from these rows. The refusals come first and by name: a
|
|
103
|
+
* node that cannot sign; a key with no live row; the only live owner key (`MinOneOwner`), ahead
|
|
104
|
+
* of `self` so a sole owner asking to remove its own key hears the rule no other owner could
|
|
105
|
+
* get past either; and this node's own key, since the schema's remove branch wants a signer
|
|
106
|
+
* other than the removed key. With this node enrolled, a different live key always leaves two
|
|
107
|
+
* owners in one read, so `last-owner` reaches only that case, and concurrent removals elsewhere
|
|
108
|
+
* leave the schema to decide.
|
|
109
|
+
*
|
|
110
|
+
* `dependents` are the rows the key vouched, which every reader judges against its anchor
|
|
111
|
+
* through `VouchOwner`: once the key is removed they stop passing the membership predicate on
|
|
112
|
+
* every machine unless re-vouched first. This node's own rows are not among them. Its own
|
|
113
|
+
* `CadrePeer` row is re-vouched unasked (`revouchesOwnRow`): left on the removed key, every other
|
|
114
|
+
* machine would drop the remover. Its own `OwnerKey` row is reported as `ownDependency`, since
|
|
115
|
+
* re-vouching it would mean signing its own removal.
|
|
116
|
+
*
|
|
117
|
+
* `grants` are the live owner grants the key issued: a redemption wants a live issuer, so they
|
|
118
|
+
* stop working with the removal. They are listed, not withdrawn (`removeOwner` leaves them: a
|
|
119
|
+
* tombstone over a grant that can no longer be redeemed would add nothing).
|
|
120
|
+
*/
|
|
121
|
+
export function planOwnerKeyRemoval(input: OwnerKeyRemovalPlanInput): OwnerKeyRemovalPlan {
|
|
122
|
+
const row = input.owners.find((owner) => owner.key === input.key) ?? null;
|
|
123
|
+
return {
|
|
124
|
+
ownerKey: row === null ? null : describeOwnerKeys([row], input.selfOwnerKey, input.anchor)[0],
|
|
125
|
+
refusal: removalRefusalFor(input, row !== null),
|
|
126
|
+
dependents: dependentsOf(input),
|
|
127
|
+
revouchesOwnRow: input.rows.some((own) => own.peerId === input.selfPeerId && own.vouchOwner === input.key),
|
|
128
|
+
ownDependency: input.owners.some((own) => own.key === input.selfOwnerKey && own.vouchOwner === input.key),
|
|
129
|
+
grants: input.grants.filter((status) => status.live && status.grant.issuerKey === input.key).map((status) => status.grant),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removalRefusalFor(input: OwnerKeyRemovalPlanInput, live: boolean): OwnerKeyRemovalPlan['refusal'] {
|
|
134
|
+
if (input.selfOwnerKey === null) {
|
|
135
|
+
return 'not-an-owner';
|
|
136
|
+
}
|
|
137
|
+
if (!live) {
|
|
138
|
+
return 'unknown';
|
|
139
|
+
}
|
|
140
|
+
if (input.owners.length <= 1) {
|
|
141
|
+
return 'last-owner';
|
|
142
|
+
}
|
|
143
|
+
if (input.key === input.selfOwnerKey) {
|
|
144
|
+
return 'self';
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function dependentsOf(input: OwnerKeyRemovalPlanInput): OwnerKeyDependents {
|
|
150
|
+
return {
|
|
151
|
+
owners: input.owners
|
|
152
|
+
.filter((owner) => owner.vouchOwner === input.key && owner.key !== input.key && owner.key !== input.selfOwnerKey)
|
|
153
|
+
.map((owner) => owner.key),
|
|
154
|
+
peers: input.rows
|
|
155
|
+
.filter((row) => row.vouchOwner === input.key && row.peerId !== input.selfPeerId)
|
|
156
|
+
.map((row) => row.peerId),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The re-vouch `CadreNode.removeOwner` makes: what the caller asked for, checked against the
|
|
162
|
+
* plan, plus this node's own row (`selfPeerId`, first) when `plan.revouchesOwnRow`. Every
|
|
163
|
+
* requested key and peer must be in `plan.dependents`, so a caller cannot re-vouch a row the
|
|
164
|
+
* removed key never vouched. Throws naming the offenders, before anything is written.
|
|
165
|
+
* Duplicates are dropped.
|
|
166
|
+
*/
|
|
167
|
+
export function revouchWithinPlan(plan: OwnerKeyRemovalPlan, revouch: RemoveOwnerOptions['revouch'], selfPeerId: string | null): OwnerKeyDependents {
|
|
168
|
+
const owners = [...new Set(revouch?.owners ?? [])];
|
|
169
|
+
const peers = [...new Set(revouch?.peers ?? [])];
|
|
170
|
+
const strayOwners = owners.filter((key) => !plan.dependents.owners.includes(key));
|
|
171
|
+
const strayPeers = peers.filter((peerId) => !plan.dependents.peers.includes(peerId));
|
|
172
|
+
if (strayOwners.length > 0 || strayPeers.length > 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`removeOwner: re-vouch names rows the removed owner key did not vouch: ${[...strayOwners, ...strayPeers].join(', ')}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return { owners, peers: plan.revouchesOwnRow && selfPeerId !== null ? [selfPeerId, ...peers] : peers };
|
|
178
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner signer: the one interface a node's owner signatures go through
|
|
3
|
+
* (`docs/architecture.md` → Owner Keys → The owner signer).
|
|
4
|
+
*
|
|
5
|
+
* cadre-core needs owner signatures, never an owner private key, so a backend that keeps the
|
|
6
|
+
* key where the node cannot read it (a hardware key, a vault behind biometrics) fits the same
|
|
7
|
+
* seam as {@link softwareOwnerSigner}.
|
|
8
|
+
*
|
|
9
|
+
* Imports only `@optimystic/quereus-plugin-crypto` and `uint8arrays`, so it loads on React
|
|
10
|
+
* Native and in the browser.
|
|
11
|
+
*/
|
|
12
|
+
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
13
|
+
import { sign } from '@optimystic/quereus-plugin-crypto';
|
|
14
|
+
import { ed25519PublicKeyFromPrivate, requireEd25519PrivateKeyB64 } from './ed25519-key.js';
|
|
15
|
+
|
|
16
|
+
/** What a backend that asks the user shows them. */
|
|
17
|
+
export interface OwnerSigningRequest {
|
|
18
|
+
/** The cadre the signatures are for. Every owner digest already binds it (`party_id()`); a prompting backend shows it. */
|
|
19
|
+
partyId: string;
|
|
20
|
+
/** What the user is approving, in plain words ("Remove the machine 12D3…", "Add owner key ‹label›"). */
|
|
21
|
+
action: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Signs one message (the raw bytes of an owner digest, e.g. `buildAuthorizationMessage` output); resolves to a base64url Ed25519 signature. */
|
|
25
|
+
export type OwnerSign = (message: Uint8Array) => Promise<string>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An owner key the node can ask for signatures.
|
|
29
|
+
*
|
|
30
|
+
* One action, one {@link approve} call, however many rows it signs: the control schema verifies
|
|
31
|
+
* one signature per row, and one owner action writes several (a removal signs the delete and its
|
|
32
|
+
* `Revocation` tombstone; a machine removal signs many). A hardware or biometric backend prompts
|
|
33
|
+
* once per call, as PIV touch caching and the platform vaults' authentication-validity windows
|
|
34
|
+
* allow. Handing the signer every digest up front was declined: each writer reads the stamps it
|
|
35
|
+
* signs inside its write lock, so precomputing them would duplicate each writer's digest
|
|
36
|
+
* construction outside it.
|
|
37
|
+
*/
|
|
38
|
+
export interface OwnerSigner {
|
|
39
|
+
/** base64url Ed25519 public key — the `OwnerKey.Key` this signer signs for. */
|
|
40
|
+
readonly publicKey: string;
|
|
41
|
+
/**
|
|
42
|
+
* Run one user-approved action. A backend that prompts does so at most once per call and may
|
|
43
|
+
* keep the user's presence until `body` settles. Rejects with {@link OwnerSigningDeclinedError}
|
|
44
|
+
* when the user declines, and with whatever `body` rejects with otherwise.
|
|
45
|
+
*/
|
|
46
|
+
approve<T>(request: OwnerSigningRequest, body: (sign: OwnerSign) => Promise<T>): Promise<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `body` inside the caller's open approval when it passes `sign`, else inside one new
|
|
51
|
+
* approval of `signer` for `request`. An owner write that is sometimes a whole action and
|
|
52
|
+
* sometimes one step of a larger one takes an optional `sign` and goes through this, so the
|
|
53
|
+
* larger action still asks once.
|
|
54
|
+
*/
|
|
55
|
+
export async function withinOwnerApproval<T>(
|
|
56
|
+
signer: OwnerSigner,
|
|
57
|
+
request: OwnerSigningRequest,
|
|
58
|
+
sign: OwnerSign | undefined,
|
|
59
|
+
body: (sign: OwnerSign) => Promise<T>
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
return sign ? await body(sign) : await signer.approve(request, body);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Thrown by a prompting backend when the user declines or the prompt times out. */
|
|
65
|
+
export class OwnerSigningDeclinedError extends Error {
|
|
66
|
+
readonly request: OwnerSigningRequest;
|
|
67
|
+
|
|
68
|
+
constructor(request: OwnerSigningRequest, options?: { cause?: unknown }) {
|
|
69
|
+
super(`Owner approval was declined or timed out: ${request.action}`, options);
|
|
70
|
+
this.name = 'OwnerSigningDeclinedError';
|
|
71
|
+
this.request = request;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An {@link OwnerSigner} over a private key held in memory: never prompts, signs whatever its
|
|
77
|
+
* `approve` body asks. Its signed input is the digest bytes, base64url-encoded, so its signatures
|
|
78
|
+
* are byte-for-byte those of any signer that signs the raw bytes with the same key (Ed25519 is
|
|
79
|
+
* deterministic).
|
|
80
|
+
*
|
|
81
|
+
* @param privateKeyB64 - The owner key's 32-byte Ed25519 seed, base64url.
|
|
82
|
+
* @throws If `privateKeyB64` is not a 32-byte seed in base64url.
|
|
83
|
+
*/
|
|
84
|
+
export function softwareOwnerSigner(privateKeyB64: string): OwnerSigner {
|
|
85
|
+
const privateKey = requireEd25519PrivateKeyB64(privateKeyB64, 'owner private key');
|
|
86
|
+
const signMessage: OwnerSign = async (message) =>
|
|
87
|
+
sign(uint8ArrayToString(message, 'base64url'), privateKey, 'ed25519', 'base64url', 'base64url', 'base64url') as string;
|
|
88
|
+
return {
|
|
89
|
+
publicKey: ed25519PublicKeyFromPrivate(privateKey),
|
|
90
|
+
async approve<T>(_request: OwnerSigningRequest, body: (sign: OwnerSign) => Promise<T>): Promise<T> {
|
|
91
|
+
return await body(signMessage);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|