@openlfcp/client 0.2.2 → 0.3.1
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/dist/data-unit.d.ts +11 -0
- package/dist/data-unit.js +9 -0
- package/dist/identity/accept.d.ts +115 -0
- package/dist/identity/accept.js +335 -0
- package/dist/identity/context.d.ts +214 -0
- package/dist/identity/context.js +495 -0
- package/dist/identity/devices.d.ts +127 -0
- package/dist/identity/devices.js +221 -0
- package/dist/identity/document.d.ts +112 -0
- package/dist/identity/document.js +54 -0
- package/dist/identity/index.d.ts +17 -0
- package/dist/identity/index.js +17 -0
- package/dist/identity/journal.d.ts +139 -0
- package/dist/identity/journal.js +202 -0
- package/dist/identity/network.d.ts +88 -0
- package/dist/identity/network.js +243 -0
- package/dist/identity/pairing-request.d.ts +147 -0
- package/dist/identity/pairing-request.js +422 -0
- package/dist/identity/pairing.d.ts +225 -0
- package/dist/identity/pairing.js +624 -0
- package/dist/identity/phases.d.ts +75 -0
- package/dist/identity/phases.js +104 -0
- package/dist/identity/proof.d.ts +45 -0
- package/dist/identity/proof.js +71 -0
- package/dist/identity/reconcile.d.ts +73 -0
- package/dist/identity/reconcile.js +273 -0
- package/dist/identity/resource.d.ts +74 -0
- package/dist/identity/resource.js +201 -0
- package/dist/identity/revocation.d.ts +106 -0
- package/dist/identity/revocation.js +403 -0
- package/dist/identity/roster.d.ts +89 -0
- package/dist/identity/roster.js +130 -0
- package/dist/identity/rotation.d.ts +131 -0
- package/dist/identity/rotation.js +363 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/revoke.d.ts +8 -0
- package/dist/revoke.js +12 -8
- package/dist/storage.d.ts +18 -0
- package/dist/storage.js +26 -0
- package/dist/sync-client.d.ts +2 -0
- package/dist/sync-client.js +1 -0
- package/package.json +5 -5
package/dist/data-unit.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface CreateDataUnitOptions<T> {
|
|
|
31
31
|
readonly previousUnitId: DataUnitId | null;
|
|
32
32
|
readonly profile: DataProfileCodec<T>;
|
|
33
33
|
readonly value: T;
|
|
34
|
+
/** The Resource's durable write barrier (writeBarrier), if any: a set barrier refuses the write (§8.6). */
|
|
35
|
+
readonly writeBarrier?: {
|
|
36
|
+
readonly state: string;
|
|
37
|
+
} | undefined;
|
|
34
38
|
}
|
|
35
39
|
export interface CreatedDataUnit {
|
|
36
40
|
/** The exact signed bytes: opaque, the only form sent to servers and peers. */
|
|
@@ -45,6 +49,13 @@ export interface CreatedDataUnit {
|
|
|
45
49
|
* Resource's, that the actor holds data/write at the head and that the DEK
|
|
46
50
|
* is the one committed for the head's current epoch; then it reserves the
|
|
47
51
|
* next sequence and seals the unit (sealDataUnit).
|
|
52
|
+
*
|
|
53
|
+
* Contract (IDENTITY-PROFILE-01 §8.6): this primitive sees no storage, so
|
|
54
|
+
* it cannot know that the device was refused or proven revoked after the
|
|
55
|
+
* view was validated. Callers that write for a stored Resource pass its
|
|
56
|
+
* durable `writeBarrier` (or call `checkWriteBarrier` first); the
|
|
57
|
+
* storage-backed paths (createQueuedDataUnit, commitOperation, the
|
|
58
|
+
* identity flows) do it themselves.
|
|
48
59
|
*/
|
|
49
60
|
export declare function createDataUnit<T>(options: CreateDataUnitOptions<T>): Promise<CreatedDataUnit>;
|
|
50
61
|
//# sourceMappingURL=data-unit.d.ts.map
|
package/dist/data-unit.js
CHANGED
|
@@ -7,8 +7,17 @@ import { ABILITY, hasAbility, sealDataUnit, } from "@openlfcp/wire";
|
|
|
7
7
|
* Resource's, that the actor holds data/write at the head and that the DEK
|
|
8
8
|
* is the one committed for the head's current epoch; then it reserves the
|
|
9
9
|
* next sequence and seals the unit (sealDataUnit).
|
|
10
|
+
*
|
|
11
|
+
* Contract (IDENTITY-PROFILE-01 §8.6): this primitive sees no storage, so
|
|
12
|
+
* it cannot know that the device was refused or proven revoked after the
|
|
13
|
+
* view was validated. Callers that write for a stored Resource pass its
|
|
14
|
+
* durable `writeBarrier` (or call `checkWriteBarrier` first); the
|
|
15
|
+
* storage-backed paths (createQueuedDataUnit, commitOperation, the
|
|
16
|
+
* identity flows) do it themselves.
|
|
10
17
|
*/
|
|
11
18
|
export async function createDataUnit(options) {
|
|
19
|
+
if (options.writeBarrier !== undefined)
|
|
20
|
+
throw new LfcpError("AUTHORIZATION_FAILED", `this device must not write to the Resource: ${options.writeBarrier.state} (IDENTITY-PROFILE-01 §8.6)`);
|
|
12
21
|
const atHead = options.view.stateAt(options.controlHead);
|
|
13
22
|
if (atHead === undefined)
|
|
14
23
|
throw new LfcpError("MISSING_DEPENDENCY", "the Control Head is not on the validated chain");
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type CertificateChain, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
|
|
3
|
+
import type { InvitationLink } from "../invite.js";
|
|
4
|
+
import { type KeyDelivery } from "./context.js";
|
|
5
|
+
import { type RegistrationResult } from "./devices.js";
|
|
6
|
+
import { type LinearChain } from "./phases.js";
|
|
7
|
+
import { type IdentityFlowOptions } from "./resource.js";
|
|
8
|
+
export interface DeviceResourceResult {
|
|
9
|
+
/** Packages to the root (§7.2 step 2, §7.3). */
|
|
10
|
+
readonly rootKeys: KeyDelivery;
|
|
11
|
+
/** The root's other devices registered here by this managing device; empty without `12`. */
|
|
12
|
+
readonly siblings: readonly RegistrationResult[];
|
|
13
|
+
/** The roster entry was written in `I`. */
|
|
14
|
+
readonly roster: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** §7.2: the claim of an invitation for the root, signed by the Invitation Principal (W §18.1 rule 5). */
|
|
17
|
+
export declare const claimForRootRecord: (chain: LinearChain, invitee: Signer, invitationGrantId: Uint8Array, root: PrincipalDescriptor, abilities: readonly bigint[]) => Uint8Array;
|
|
18
|
+
/** §7.2 step 1: the device's own first registration (W §17.4 rule 6). */
|
|
19
|
+
export declare const selfRegistrationRecord: (chain: LinearChain, device: Signer, certificates: CertificateChain) => Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* The Resource is joined or created and keyed, but its roster entry could
|
|
22
|
+
* not be written: the operation stays open; resume it with `operation`
|
|
23
|
+
* (§7.2 step 4, §7.3).
|
|
24
|
+
*/
|
|
25
|
+
export interface IncompleteDeviceResource extends DeviceResourceResult {
|
|
26
|
+
readonly kind: "incomplete";
|
|
27
|
+
readonly operation: string;
|
|
28
|
+
readonly resource: ResourceId;
|
|
29
|
+
}
|
|
30
|
+
export type DeviceAcceptResult = IncompleteDeviceResource | ({
|
|
31
|
+
readonly kind: "claimed";
|
|
32
|
+
readonly operation: string;
|
|
33
|
+
readonly resource: ResourceId;
|
|
34
|
+
/** The claim record: the root's grant (§18.1). */
|
|
35
|
+
readonly grantId: Uint8Array;
|
|
36
|
+
} & DeviceResourceResult)
|
|
37
|
+
/**
|
|
38
|
+
* This device cannot write the roster (`holds(D_B, 2)` in `I` is false,
|
|
39
|
+
* e.g. a Read device): the invitation stays unclaimed; complete it on an
|
|
40
|
+
* installation that can write `I` (§7.2).
|
|
41
|
+
*/
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "needs-writer";
|
|
44
|
+
}
|
|
45
|
+
/** The coordinator refused the claim (e.g. the invitation is used up). */
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "refused";
|
|
48
|
+
readonly operation: string;
|
|
49
|
+
readonly code: string;
|
|
50
|
+
}
|
|
51
|
+
/** The link has no secret, or its secret is not the grant's subject (W §18.2). */
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "invalid-invitation";
|
|
54
|
+
readonly reason: string;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: "conflict";
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: "unavailable";
|
|
59
|
+
readonly operation?: string;
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* §7.2: accepts an invitation on a device `D_B` of root `R`. The claim
|
|
64
|
+
* names `R` (claimant = `R`'s descriptor, known from `I`'s Genesis) and is
|
|
65
|
+
* signed by the Invitation Principal; then, as one durable operation, the
|
|
66
|
+
* device registers itself (W §17.4 rule 6), seals every epoch it holds to
|
|
67
|
+
* `R`, registers and keys `R`'s other devices when it has `12`, and adds
|
|
68
|
+
* the Resource to the roster. When the installation is the root itself,
|
|
69
|
+
* the claim names it as today and only the last two steps remain.
|
|
70
|
+
*/
|
|
71
|
+
export declare function acceptInvitationAsDevice(o: IdentityFlowOptions & {
|
|
72
|
+
readonly link: string | InvitationLink;
|
|
73
|
+
/** The coordinator to use (default: the link's first endpoint). */
|
|
74
|
+
readonly url?: string;
|
|
75
|
+
readonly name?: string;
|
|
76
|
+
/** Resume this operation. */
|
|
77
|
+
readonly operation?: string;
|
|
78
|
+
}): Promise<DeviceAcceptResult>;
|
|
79
|
+
export type DeviceCreateResult = IncompleteDeviceResource | ({
|
|
80
|
+
readonly kind: "created";
|
|
81
|
+
readonly operation: string;
|
|
82
|
+
readonly resource: ResourceId;
|
|
83
|
+
readonly chain: LinearChain;
|
|
84
|
+
} & DeviceResourceResult)
|
|
85
|
+
/** Only a personal device with `13` creates Resources for its root (§7.3). */
|
|
86
|
+
| {
|
|
87
|
+
readonly kind: "not-owner-acting";
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: "refused";
|
|
90
|
+
readonly operation: string;
|
|
91
|
+
readonly code: string;
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: "conflict";
|
|
94
|
+
} | {
|
|
95
|
+
readonly kind: "unavailable";
|
|
96
|
+
readonly operation?: string;
|
|
97
|
+
readonly reason: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* §7.3: a personal device with `13` creates a Resource owned by its root:
|
|
101
|
+
* Genesis field 1 = the root's descriptor, field 5 = the device's chain,
|
|
102
|
+
* signed by the device (W §15), hosted by the device's session; then the
|
|
103
|
+
* epoch-0 DEK sealed to the root, the root's other devices registered and
|
|
104
|
+
* keyed when the device has `12`, and the roster entry — so the root opens
|
|
105
|
+
* the Resource even if the creating device goes offline right after.
|
|
106
|
+
*/
|
|
107
|
+
export declare function createResourceAsDevice(o: IdentityFlowOptions & {
|
|
108
|
+
readonly dataProfile: string;
|
|
109
|
+
/** Route and coordinator of the new Resource. */
|
|
110
|
+
readonly url: string;
|
|
111
|
+
readonly name?: string;
|
|
112
|
+
/** Resume this operation. */
|
|
113
|
+
readonly operation?: string;
|
|
114
|
+
}): Promise<DeviceCreateResult>;
|
|
115
|
+
//# sourceMappingURL=accept.d.ts.map
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { bytesEqual, dataEpoch, fromHex, generateResourceId, principalId, resourceId, secureRandom, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { dekCommitment, generateResourceDEK } from "@openlfcp/crypto";
|
|
3
|
+
import { ABILITY, activeDevice, DEVICE_ABILITY, DEVICE_KIND, hasAbility, invitationPrincipal, parseInviteUri, signControlRecord, validateControlChain, verifyInvitationSecret, } from "@openlfcp/wire";
|
|
4
|
+
import { deliverKeys, fetchDeks, genesisOwner, IdentityConflict, runControlPhase, storeDek, syncChain, } from "./context.js";
|
|
5
|
+
import { registerDevice } from "./devices.js";
|
|
6
|
+
import { newOperationId, OperationJournal, PackageLedger, phaseOf, } from "./journal.js";
|
|
7
|
+
import { ChannelPool, IdentityRefused, IdentityUnavailable } from "./network.js";
|
|
8
|
+
import { classifyRegister } from "./phases.js";
|
|
9
|
+
import { identityTarget } from "./resource.js";
|
|
10
|
+
import { writeRosterEntry } from "./roster.js";
|
|
11
|
+
/**
|
|
12
|
+
* Invitations accepted by a device and Resources created from a device
|
|
13
|
+
* (IDENTITY-PROFILE-01 §7.2, §7.3). Inviting is unchanged; a device that
|
|
14
|
+
* accepts claims for its root, registers itself, keys the root and (as a
|
|
15
|
+
* managing device) its siblings, and adds the Resource to the roster, as
|
|
16
|
+
* one durable operation, so no accepted or created Resource is ever
|
|
17
|
+
* unreachable for the human's other installations.
|
|
18
|
+
*/
|
|
19
|
+
/** The root's descriptor: `I`'s Genesis owner (§7.2). */
|
|
20
|
+
function rootDescriptor(I) {
|
|
21
|
+
const g = I.records[0];
|
|
22
|
+
if (g?.body.type !== "GENESIS")
|
|
23
|
+
throw new Error("the Identity Resource's chain has no Genesis");
|
|
24
|
+
return g.body.owner;
|
|
25
|
+
}
|
|
26
|
+
/** This installation's chain in `I`, when it is an active personal device of the root. */
|
|
27
|
+
function myChain(I, me) {
|
|
28
|
+
const d = activeDevice(I.state, me);
|
|
29
|
+
return d !== undefined && d.kind === DEVICE_KIND.PERSONAL && d.chain !== null
|
|
30
|
+
? d.chain
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
/** The other active personal devices of the root in `I`, with their chains (§7.2 step 3, §7.3). */
|
|
34
|
+
function siblings(I, me) {
|
|
35
|
+
const R = genesisOwner(I);
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const [hex, d] of I.state.devices) {
|
|
38
|
+
const p = principalId(fromHex(hex));
|
|
39
|
+
if (bytesEqual(p, me) || d.kind !== DEVICE_KIND.PERSONAL || d.chain === null)
|
|
40
|
+
continue;
|
|
41
|
+
if (!bytesEqual(d.root, R) || activeDevice(I.state, p) === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
out.push(d.chain);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The common tail of §7.2 steps 2–4 and §7.3: seals every epoch held of
|
|
49
|
+
* `X` to the root, registers and keys the root's other active personal
|
|
50
|
+
* devices when this installation manages devices, and adds `X` to the
|
|
51
|
+
* roster of `I`. Each part is idempotent (ledger, §9.2 classification,
|
|
52
|
+
* journaled unit), so a resumed operation repeats nothing.
|
|
53
|
+
*/
|
|
54
|
+
async function keyAndList(o, pool, journal, operation, I, iUrl, X, url, entry) {
|
|
55
|
+
const me = o.installation.signer;
|
|
56
|
+
const R = rootDescriptor(I);
|
|
57
|
+
const channel = await pool.get(me, url);
|
|
58
|
+
const chainX = await syncChain(o.storage, channel, X);
|
|
59
|
+
const deks = await fetchDeks(o.storage, o.secrets, channel, chainX, o.installation);
|
|
60
|
+
const rootKeys = bytesEqual(R.principalId, me.descriptor.principalId)
|
|
61
|
+
? { sent: [], complete: 0, skipped: [] }
|
|
62
|
+
: await deliverKeys({
|
|
63
|
+
channel,
|
|
64
|
+
ledger: new PackageLedger(o.storage),
|
|
65
|
+
chain: chainX,
|
|
66
|
+
sender: me,
|
|
67
|
+
recipients: [R],
|
|
68
|
+
deks,
|
|
69
|
+
});
|
|
70
|
+
const mine = activeDevice(I.state, me.descriptor.principalId);
|
|
71
|
+
const manages = bytesEqual(R.principalId, me.descriptor.principalId) ||
|
|
72
|
+
mine?.mask.includes(DEVICE_ABILITY.DEVICE_MANAGE);
|
|
73
|
+
const others = [];
|
|
74
|
+
if (manages)
|
|
75
|
+
for (const chain of siblings(I, me.descriptor.principalId)) {
|
|
76
|
+
const r = await registerDevice({ ...o, chain, targets: [{ resource: X, url }] });
|
|
77
|
+
others.push(...r.results);
|
|
78
|
+
}
|
|
79
|
+
// §7.2 step 4, §7.3: the roster entry, so the human's other installations reach X.
|
|
80
|
+
const written = await writeRosterEntry(o, await pool.get(me, iUrl), journal, operation, I.state.resourceId, X, entry);
|
|
81
|
+
const roster = written.state !== "refused";
|
|
82
|
+
return { rootKeys, siblings: Object.freeze(others), roster };
|
|
83
|
+
}
|
|
84
|
+
/** §7.2: the claim of an invitation for the root, signed by the Invitation Principal (W §18.1 rule 5). */
|
|
85
|
+
export const claimForRootRecord = (chain, invitee, invitationGrantId, root, abilities) => signControlRecord({
|
|
86
|
+
resourceId: chain.state.resourceId,
|
|
87
|
+
controlSeq: chain.state.seq + 1n,
|
|
88
|
+
prevControlId: chain.state.head,
|
|
89
|
+
}, {
|
|
90
|
+
type: "CAPABILITY_CLAIM",
|
|
91
|
+
invitationGrantId: invitationGrantId,
|
|
92
|
+
claimant: root,
|
|
93
|
+
abilities,
|
|
94
|
+
}, invitee).bytes;
|
|
95
|
+
/** §7.2 step 1: the device's own first registration (W §17.4 rule 6). */
|
|
96
|
+
export const selfRegistrationRecord = (chain, device, certificates) => signControlRecord({
|
|
97
|
+
resourceId: chain.state.resourceId,
|
|
98
|
+
controlSeq: chain.state.seq + 1n,
|
|
99
|
+
prevControlId: chain.state.head,
|
|
100
|
+
}, { type: "DEVICE_GRANT", root: certificates.root, chain: certificates }, device).bytes;
|
|
101
|
+
/**
|
|
102
|
+
* §7.2: accepts an invitation on a device `D_B` of root `R`. The claim
|
|
103
|
+
* names `R` (claimant = `R`'s descriptor, known from `I`'s Genesis) and is
|
|
104
|
+
* signed by the Invitation Principal; then, as one durable operation, the
|
|
105
|
+
* device registers itself (W §17.4 rule 6), seals every epoch it holds to
|
|
106
|
+
* `R`, registers and keys `R`'s other devices when it has `12`, and adds
|
|
107
|
+
* the Resource to the roster. When the installation is the root itself,
|
|
108
|
+
* the claim names it as today and only the last two steps remain.
|
|
109
|
+
*/
|
|
110
|
+
export async function acceptInvitationAsDevice(o) {
|
|
111
|
+
const invitation = parseInviteUri(typeof o.link === "string" ? o.link : o.link.reveal());
|
|
112
|
+
const secret = invitation.secret;
|
|
113
|
+
if (secret === undefined)
|
|
114
|
+
return { kind: "invalid-invitation", reason: "the link has no #secret= fragment" };
|
|
115
|
+
const X = invitation.resourceId;
|
|
116
|
+
const url = o.url ?? invitation.endpoints[0];
|
|
117
|
+
const me = o.installation.signer;
|
|
118
|
+
const target = await identityTarget(o);
|
|
119
|
+
const journal = new OperationJournal(o.storage);
|
|
120
|
+
const pool = new ChannelPool(o.network);
|
|
121
|
+
let operation = o.operation;
|
|
122
|
+
try {
|
|
123
|
+
const iChannel = await pool.get(me, target.url);
|
|
124
|
+
const I = await syncChain(o.storage, iChannel, target.resource);
|
|
125
|
+
const R = rootDescriptor(I);
|
|
126
|
+
const isRoot = bytesEqual(R.principalId, me.descriptor.principalId);
|
|
127
|
+
const chainB = isRoot ? undefined : myChain(I, me.descriptor.principalId);
|
|
128
|
+
// §7.2: no claim unless this device can write the roster entry in I.
|
|
129
|
+
if (!isRoot &&
|
|
130
|
+
(chainB === undefined || !hasAbility(I.state, me.descriptor.principalId, ABILITY.DATA_WRITE)))
|
|
131
|
+
return { kind: "needs-writer" };
|
|
132
|
+
// The Invitation Principal opens X, verifies its secret and opens its package (W §73).
|
|
133
|
+
const invitee = { key: secret.signingKey, descriptor: invitationPrincipal(secret) };
|
|
134
|
+
const ch = await pool.get(invitee, url);
|
|
135
|
+
let chainX = await syncChain(o.storage, ch, X);
|
|
136
|
+
const claimSigner = verifyInvitationSecretOn(chainX, invitation.grantId, secret);
|
|
137
|
+
if (typeof claimSigner === "string")
|
|
138
|
+
return { kind: "invalid-invitation", reason: claimSigner };
|
|
139
|
+
await fetchDeks(o.storage, o.secrets, ch, chainX, {
|
|
140
|
+
signer: invitee,
|
|
141
|
+
agreement: secret.agreementKey,
|
|
142
|
+
});
|
|
143
|
+
const grant = chainX.state.grants.get(toHex(invitation.grantId));
|
|
144
|
+
if (grant === undefined)
|
|
145
|
+
return { kind: "invalid-invitation", reason: "the grant is not on the chain" };
|
|
146
|
+
const delegable = grant.delegable.includes(ABILITY.INVITE_CLAIM);
|
|
147
|
+
const abilities = grant.abilities.filter((a) => a !== ABILITY.INVITE_CLAIM || delegable);
|
|
148
|
+
if (operation === undefined || (await journal.get(operation)) === undefined) {
|
|
149
|
+
operation = operation ?? newOperationId("accept-invitation", secureRandom(8));
|
|
150
|
+
await journal.begin({
|
|
151
|
+
id: operation,
|
|
152
|
+
kind: "accept-invitation",
|
|
153
|
+
createdAt: o.now(),
|
|
154
|
+
intent: {
|
|
155
|
+
resource: toHex(X),
|
|
156
|
+
url,
|
|
157
|
+
grant: toHex(invitation.grantId),
|
|
158
|
+
root: toHex(R.principalId),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const op = operation;
|
|
163
|
+
// The claim, for R (W §18.1 rule 5): a claim already on the chain for R is this operation's or complete.
|
|
164
|
+
const claim = await runControlPhase(o.storage, ch, journal, op, {
|
|
165
|
+
id: "claim",
|
|
166
|
+
kind: "claim",
|
|
167
|
+
resource: X,
|
|
168
|
+
classify: (chain, isOwn) => {
|
|
169
|
+
const done = chain.records.find((r) => r.body.type === "CAPABILITY_CLAIM" &&
|
|
170
|
+
bytesEqual(r.body.invitationGrantId, invitation.grantId) &&
|
|
171
|
+
bytesEqual(r.body.claimant.principalId, R.principalId));
|
|
172
|
+
if (done === undefined)
|
|
173
|
+
return "pending";
|
|
174
|
+
return isOwn(toHex(done.signed.id)) ? "committed-own" : "complete";
|
|
175
|
+
},
|
|
176
|
+
build: (chain) => claimForRootRecord(chain, claimSigner, invitation.grantId, R, abilities),
|
|
177
|
+
});
|
|
178
|
+
if (claim.state === "refused") {
|
|
179
|
+
await journal.setStatus(op, "abandoned");
|
|
180
|
+
return { kind: "refused", operation: op, code: claim.code ?? "refused" };
|
|
181
|
+
}
|
|
182
|
+
chainX = claim.chain;
|
|
183
|
+
const grantId = chainX.records.find((r) => r.body.type === "CAPABILITY_CLAIM" &&
|
|
184
|
+
bytesEqual(r.body.invitationGrantId, invitation.grantId))?.signed.id;
|
|
185
|
+
// Step 1: self-registration (W §17.4 rule 6), proposed through the Invitation Principal's session.
|
|
186
|
+
if (chainB !== undefined) {
|
|
187
|
+
const reg = await runControlPhase(o.storage, ch, journal, op, {
|
|
188
|
+
id: `register:${toHex(X)}`,
|
|
189
|
+
kind: "register",
|
|
190
|
+
resource: X,
|
|
191
|
+
classify: (chain, isOwn) => classifyRegister(chain, { root: R.principalId, chain: chainB, isOwnRecord: isOwn }),
|
|
192
|
+
build: (chain) => selfRegistrationRecord(chain, me, chainB),
|
|
193
|
+
});
|
|
194
|
+
if (reg.state === "refused" || reg.state === "superseded")
|
|
195
|
+
return { kind: "refused", operation: op, code: reg.code ?? reg.state };
|
|
196
|
+
}
|
|
197
|
+
// Steps 2–4.
|
|
198
|
+
const rest = await keyAndList(o, pool, journal, op, I, target.url, X, url, {
|
|
199
|
+
profile: chainX.state.dataProfile,
|
|
200
|
+
endpoints: [...invitation.endpoints],
|
|
201
|
+
coordinator: chainX.state.route.coordinatorUrl,
|
|
202
|
+
...(o.name === undefined ? {} : { name: o.name }),
|
|
203
|
+
});
|
|
204
|
+
// The roster entry is the point of §7.2: without it the operation stays open.
|
|
205
|
+
if (!rest.roster)
|
|
206
|
+
return { kind: "incomplete", operation: op, resource: X, ...rest };
|
|
207
|
+
await journal.setStatus(op, "done");
|
|
208
|
+
return { kind: "claimed", operation: op, resource: X, grantId, ...rest };
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
if (e instanceof IdentityUnavailable || e instanceof IdentityRefused)
|
|
212
|
+
return {
|
|
213
|
+
kind: "unavailable",
|
|
214
|
+
...(operation === undefined ? {} : { operation }),
|
|
215
|
+
reason: e.message,
|
|
216
|
+
};
|
|
217
|
+
if (e instanceof IdentityConflict)
|
|
218
|
+
return { kind: "conflict" };
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
pool.closeAll();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function verifyInvitationSecretOn(chain, grantId, secret) {
|
|
226
|
+
try {
|
|
227
|
+
return verifyInvitationSecret(chain.state, grantId, secret);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return e instanceof Error ? e.message : String(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* §7.3: a personal device with `13` creates a Resource owned by its root:
|
|
235
|
+
* Genesis field 1 = the root's descriptor, field 5 = the device's chain,
|
|
236
|
+
* signed by the device (W §15), hosted by the device's session; then the
|
|
237
|
+
* epoch-0 DEK sealed to the root, the root's other devices registered and
|
|
238
|
+
* keyed when the device has `12`, and the roster entry — so the root opens
|
|
239
|
+
* the Resource even if the creating device goes offline right after.
|
|
240
|
+
*/
|
|
241
|
+
export async function createResourceAsDevice(o) {
|
|
242
|
+
const me = o.installation.signer;
|
|
243
|
+
const target = await identityTarget(o);
|
|
244
|
+
const journal = new OperationJournal(o.storage);
|
|
245
|
+
const pool = new ChannelPool(o.network);
|
|
246
|
+
let operation = o.operation;
|
|
247
|
+
try {
|
|
248
|
+
const iChannel = await pool.get(me, target.url);
|
|
249
|
+
const I = await syncChain(o.storage, iChannel, target.resource);
|
|
250
|
+
const R = rootDescriptor(I);
|
|
251
|
+
let op;
|
|
252
|
+
if (operation === undefined || (await journal.get(operation)) === undefined) {
|
|
253
|
+
const chain = myChain(I, me.descriptor.principalId);
|
|
254
|
+
if (chain === undefined || !chain.leaf.payload.mask.includes(DEVICE_ABILITY.OWNER_ACT))
|
|
255
|
+
return { kind: "not-owner-acting" };
|
|
256
|
+
const X = generateResourceId();
|
|
257
|
+
const dek = generateResourceDEK();
|
|
258
|
+
const genesis = signControlRecord({ resourceId: X, controlSeq: 0n, prevControlId: null }, {
|
|
259
|
+
type: "GENESIS",
|
|
260
|
+
dataProfile: o.dataProfile,
|
|
261
|
+
owner: R,
|
|
262
|
+
dekCommitment: dekCommitment(X, dataEpoch(0n), dek),
|
|
263
|
+
endpoints: [{ url: o.url, priority: 0n }],
|
|
264
|
+
coordinatorUrl: o.url,
|
|
265
|
+
deviceChain: chain,
|
|
266
|
+
}, me);
|
|
267
|
+
const start = validateControlChain([genesis.bytes]);
|
|
268
|
+
if (start.kind !== "linear")
|
|
269
|
+
throw new Error(`the Genesis does not validate: ${start.kind === "invalid" ? start.error.message : start.kind}`);
|
|
270
|
+
// The DEK first, then the journal that names the Genesis (§9.1).
|
|
271
|
+
await storeDek(o.storage, o.secrets, start, dataEpoch(0n), dek);
|
|
272
|
+
operation = operation ?? newOperationId("create-resource", secureRandom(8));
|
|
273
|
+
op = await journal.begin({
|
|
274
|
+
id: operation,
|
|
275
|
+
kind: "create-resource",
|
|
276
|
+
createdAt: o.now(),
|
|
277
|
+
intent: {
|
|
278
|
+
resource: toHex(X),
|
|
279
|
+
url: o.url,
|
|
280
|
+
profile: o.dataProfile,
|
|
281
|
+
...(o.name === undefined ? {} : { name: o.name }),
|
|
282
|
+
},
|
|
283
|
+
phases: [
|
|
284
|
+
{
|
|
285
|
+
id: "host",
|
|
286
|
+
kind: "host",
|
|
287
|
+
resource: toHex(X),
|
|
288
|
+
state: "pending",
|
|
289
|
+
bytes: toHex(genesis.bytes),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else
|
|
295
|
+
op = await journal.load(operation);
|
|
296
|
+
const X = resourceId(fromHex(op.intent.resource));
|
|
297
|
+
const url = op.intent.url;
|
|
298
|
+
const host = phaseOf(op, "host");
|
|
299
|
+
if (host !== undefined && host.state !== "committed-own") {
|
|
300
|
+
const answer = await (await pool.get(me, url)).host(fromHex(host.bytes));
|
|
301
|
+
if (answer.kind === "nack") {
|
|
302
|
+
await journal.putPhase(op.id, { ...host, state: "refused", code: answer.code });
|
|
303
|
+
await journal.setStatus(op.id, "abandoned");
|
|
304
|
+
return { kind: "refused", operation: op.id, code: answer.code };
|
|
305
|
+
}
|
|
306
|
+
await journal.putPhase(op.id, { ...host, state: "committed-own" });
|
|
307
|
+
}
|
|
308
|
+
const rest = await keyAndList(o, pool, journal, op.id, I, target.url, X, url, {
|
|
309
|
+
profile: op.intent.profile,
|
|
310
|
+
endpoints: [url],
|
|
311
|
+
coordinator: url,
|
|
312
|
+
...(op.intent.name === undefined ? {} : { name: op.intent.name }),
|
|
313
|
+
});
|
|
314
|
+
const chain = await syncChain(o.storage, await pool.get(me, url), X);
|
|
315
|
+
if (!rest.roster)
|
|
316
|
+
return { kind: "incomplete", operation: op.id, resource: X, ...rest };
|
|
317
|
+
await journal.setStatus(op.id, "done");
|
|
318
|
+
return { kind: "created", operation: op.id, resource: X, chain, ...rest };
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
if (e instanceof IdentityUnavailable || e instanceof IdentityRefused)
|
|
322
|
+
return {
|
|
323
|
+
kind: "unavailable",
|
|
324
|
+
...(operation === undefined ? {} : { operation }),
|
|
325
|
+
reason: e.message,
|
|
326
|
+
};
|
|
327
|
+
if (e instanceof IdentityConflict)
|
|
328
|
+
return { kind: "conflict" };
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
pool.closeAll();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=accept.js.map
|