@interop/wallet-core 0.83.0 → 0.84.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 +13 -3
- package/dist/menders/derive.d.ts +1 -0
- package/dist/menders/derive.d.ts.map +1 -1
- package/dist/menders/derive.js +4 -1
- package/dist/menders/derive.js.map +1 -1
- package/dist/menders/ids.d.ts +1 -1
- package/dist/menders/ids.d.ts.map +1 -1
- package/dist/menders/ids.js +3 -1
- package/dist/menders/ids.js.map +1 -1
- package/dist/menders/vocabulary.d.ts +4 -2
- package/dist/menders/vocabulary.d.ts.map +1 -1
- package/dist/menders/vocabulary.js +5 -2
- package/dist/menders/vocabulary.js.map +1 -1
- package/dist/space/collections.d.ts +17 -2
- package/dist/space/collections.d.ts.map +1 -1
- package/dist/space/collections.js +26 -2
- package/dist/space/collections.js.map +1 -1
- package/dist/space/index.d.ts +1 -1
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +1 -1
- package/dist/space/index.js.map +1 -1
- package/dist/writers/entry.d.ts +95 -0
- package/dist/writers/entry.d.ts.map +1 -0
- package/dist/writers/entry.js +90 -0
- package/dist/writers/entry.js.map +1 -0
- package/dist/writers/index.d.ts +17 -0
- package/dist/writers/index.d.ts.map +1 -0
- package/dist/writers/index.js +14 -0
- package/dist/writers/index.js.map +1 -0
- package/dist/writers/roster.d.ts +152 -0
- package/dist/writers/roster.d.ts.map +1 -0
- package/dist/writers/roster.js +365 -0
- package/dist/writers/roster.js.map +1 -0
- package/dist/writers/store.d.ts +71 -0
- package/dist/writers/store.d.ts.map +1 -0
- package/dist/writers/store.js +55 -0
- package/dist/writers/store.js.map +1 -0
- package/package.json +11 -5
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { RegisteredWriterEntry, RegisteredWriterPolicy } from './entry.js';
|
|
2
|
+
import type { RegisteredWritersStore } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* The writer first-seen record, kept client-local: the account and
|
|
5
|
+
* `writerId` this client's writer first appeared under, and when it last
|
|
6
|
+
* touched its roster entry. One slot, so a writer that moves to another
|
|
7
|
+
* account starts over.
|
|
8
|
+
*/
|
|
9
|
+
export interface WriterFirstSeenRecord {
|
|
10
|
+
accountDid: string;
|
|
11
|
+
writerId: string;
|
|
12
|
+
/**
|
|
13
|
+
* ISO 8601 UTC start of the session the record was written in.
|
|
14
|
+
*/
|
|
15
|
+
firstSessionAt: string;
|
|
16
|
+
/**
|
|
17
|
+
* ISO 8601 UTC time of the latest successful registration or touch.
|
|
18
|
+
*/
|
|
19
|
+
lastTouchedAt?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The client-local seam for the writer first-seen record. Where it lives is
|
|
23
|
+
* the wallet's choice. A forget of this client's local state clears it along with
|
|
24
|
+
* the `writerId`.
|
|
25
|
+
*/
|
|
26
|
+
export interface WriterFirstSeenStore {
|
|
27
|
+
get(): Promise<unknown>;
|
|
28
|
+
put(record: WriterFirstSeenRecord): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What {@link registerWriterOnSecondSession} did.
|
|
32
|
+
*
|
|
33
|
+
* - `deferred` -- the writer has not yet been encountered in a second session
|
|
34
|
+
* under this account; the first-seen record is (re)written and the roster
|
|
35
|
+
* is untouched.
|
|
36
|
+
* - `fresh` -- the latest touch is within the touch interval; no I/O.
|
|
37
|
+
* - `registered` -- a new entry was written (or a malformed one replaced).
|
|
38
|
+
* - `touched` -- the existing entry's `lastSeen` was advanced.
|
|
39
|
+
* - `raced` -- every attempt lost a conditional write; the next call retries.
|
|
40
|
+
*/
|
|
41
|
+
export type WriterRegistrationOutcome = 'deferred' | 'fresh' | 'registered' | 'touched' | 'raced';
|
|
42
|
+
/**
|
|
43
|
+
* Registers this client's writer in the roster once it is encountered in a
|
|
44
|
+
* second session under the account, and touches its `lastSeen` at most once
|
|
45
|
+
* per touch interval after that. Call it on the wallet's connect or sync path;
|
|
46
|
+
* calls within the interval cost no I/O.
|
|
47
|
+
*
|
|
48
|
+
* A new entry takes `label`. A touch keeps the stored label, so a rename
|
|
49
|
+
* stands, and refreshes `signingKeyMultibase` when one is given. Registering
|
|
50
|
+
* a new entry first makes room under the entry cap, deleting the
|
|
51
|
+
* oldest-`lastSeen` entries.
|
|
52
|
+
*
|
|
53
|
+
* @param options {object}
|
|
54
|
+
* @param options.store {RegisteredWritersStore}
|
|
55
|
+
* @param options.local {WriterFirstSeenStore}
|
|
56
|
+
* @param options.accountDid {string} the account whose Space holds the roster
|
|
57
|
+
* @param options.writerId {string} this client's writer
|
|
58
|
+
* @param options.label {string} the coarse platform self-description a new
|
|
59
|
+
* entry takes
|
|
60
|
+
* @param options.sessionStartedAt {Date} when the current session began; a
|
|
61
|
+
* first-seen record written at or after it is this session's own
|
|
62
|
+
* @param [options.signingKeyMultibase] {string} an enrolled wallet client's
|
|
63
|
+
* own signing-key multibase
|
|
64
|
+
* @param [options.now] {Date}
|
|
65
|
+
* @param [options.policy] {Partial<RegisteredWriterPolicy>}
|
|
66
|
+
* @returns {Promise<WriterRegistrationOutcome>}
|
|
67
|
+
*/
|
|
68
|
+
export declare function registerWriterOnSecondSession({ store, local, accountDid, writerId, label, sessionStartedAt, signingKeyMultibase, now, policy }: {
|
|
69
|
+
store: RegisteredWritersStore;
|
|
70
|
+
local: WriterFirstSeenStore;
|
|
71
|
+
accountDid: string;
|
|
72
|
+
writerId: string;
|
|
73
|
+
label: string;
|
|
74
|
+
sessionStartedAt: Date;
|
|
75
|
+
signingKeyMultibase?: string;
|
|
76
|
+
now?: Date;
|
|
77
|
+
policy?: Partial<RegisteredWriterPolicy>;
|
|
78
|
+
}): Promise<WriterRegistrationOutcome>;
|
|
79
|
+
/**
|
|
80
|
+
* Renames this client's writer. The rename lands only on a registered entry;
|
|
81
|
+
* before registration there is nothing to rename, and the call returns
|
|
82
|
+
* `false`.
|
|
83
|
+
*
|
|
84
|
+
* @param options {object}
|
|
85
|
+
* @param options.store {RegisteredWritersStore}
|
|
86
|
+
* @param options.accountDid {string}
|
|
87
|
+
* @param options.writerId {string}
|
|
88
|
+
* @param options.label {string} the new label; blank is refused
|
|
89
|
+
* @returns {Promise<boolean>} whether an entry was renamed
|
|
90
|
+
*/
|
|
91
|
+
export declare function renameRegisteredWriter({ store, accountDid, writerId, label }: {
|
|
92
|
+
store: RegisteredWritersStore;
|
|
93
|
+
accountDid: string;
|
|
94
|
+
writerId: string;
|
|
95
|
+
label: string;
|
|
96
|
+
}): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* The outcome of a sweep: the entries left standing, and what it changed.
|
|
99
|
+
*/
|
|
100
|
+
export interface RegisteredWritersSweep {
|
|
101
|
+
entries: RegisteredWriterEntry[];
|
|
102
|
+
markedInactive: string[];
|
|
103
|
+
deleted: string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reads the roster and sweeps it: the sweep-on-read every history view runs.
|
|
107
|
+
* Safe to run from any replica, and concurrently.
|
|
108
|
+
*
|
|
109
|
+
* @param options {object}
|
|
110
|
+
* @param options.store {RegisteredWritersStore}
|
|
111
|
+
* @param options.accountDid {string} the account whose Space holds the roster
|
|
112
|
+
* @param [options.now] {Date}
|
|
113
|
+
* @param [options.policy] {Partial<RegisteredWriterPolicy>}
|
|
114
|
+
* @returns {Promise<RegisteredWritersSweep>}
|
|
115
|
+
*/
|
|
116
|
+
export declare function sweepRegisteredWriters({ store, accountDid, now, policy }: {
|
|
117
|
+
store: RegisteredWritersStore;
|
|
118
|
+
accountDid: string;
|
|
119
|
+
now?: Date;
|
|
120
|
+
policy?: Partial<RegisteredWriterPolicy>;
|
|
121
|
+
}): Promise<RegisteredWritersSweep>;
|
|
122
|
+
/**
|
|
123
|
+
* How a history view renders one revision's writer. `undefined` from
|
|
124
|
+
* {@link resolveRegisteredWriter} means no entry: the view renders its
|
|
125
|
+
* fallback label ("another session").
|
|
126
|
+
*/
|
|
127
|
+
export interface ResolvedWriter {
|
|
128
|
+
label: string;
|
|
129
|
+
active: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Present only when the entry's `signingKeyMultibase` is an enrolled
|
|
132
|
+
* client's key in the verified account document. The view may then link
|
|
133
|
+
* the revision to that client's row. Absent, the label stands unlinked.
|
|
134
|
+
*/
|
|
135
|
+
clientSigningKeyMultibase?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolves a revision's `writerId` against the swept roster.
|
|
139
|
+
*
|
|
140
|
+
* @param options {object}
|
|
141
|
+
* @param options.writerId {string}
|
|
142
|
+
* @param options.entries {RegisteredWriterEntry[]} a sweep's `entries`
|
|
143
|
+
* @param options.enrolledSigningKeys {ReadonlySet<string>} the signing-key
|
|
144
|
+
* multibases of the enrolled clients in the verified account document
|
|
145
|
+
* @returns {ResolvedWriter | undefined}
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolveRegisteredWriter({ writerId, entries, enrolledSigningKeys }: {
|
|
148
|
+
writerId: string;
|
|
149
|
+
entries: RegisteredWriterEntry[];
|
|
150
|
+
enrolledSigningKeys: ReadonlySet<string>;
|
|
151
|
+
}): ResolvedWriter | undefined;
|
|
152
|
+
//# sourceMappingURL=roster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../../src/writers/roster.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,YAAY,CAAA;AAEhF;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,cAAc,EAAE,MAAM,CAAA;IACtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;IACvB,GAAG,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,yBAAyB,GACnC,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,CAAA;AAyI3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,KAAK,EACL,KAAK,EACL,UAAU,EACV,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,mBAAmB,EACnB,GAAgB,EAChB,MAAM,EACP,EAAE;IACD,KAAK,EAAE,sBAAsB,CAAA;IAC7B,KAAK,EAAE,oBAAoB,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,IAAI,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,MAAM,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;CACzC,GAAG,OAAO,CAAC,yBAAyB,CAAC,CA0ErC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,QAAQ,EACR,KAAK,EACN,EAAE;IACD,KAAK,EAAE,sBAAsB,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBnB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiGD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,GAAgB,EAChB,MAAM,EACP,EAAE;IACD,KAAK,EAAE,sBAAsB,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,MAAM,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;CACzC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAUlC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,OAAO,EACP,mBAAmB,EACpB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACzC,GAAG,cAAc,GAAG,SAAS,CAa7B"}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The writer roster's logic: lazy registration with its throttled liveness
|
|
6
|
+
* touch, the user's rename, the sweep-on-read with two-phase expiry, and the
|
|
7
|
+
* join a history view renders a revision's `writerId` through.
|
|
8
|
+
*
|
|
9
|
+
* Orphaning is the normal lifecycle of a writer: a cleared browser profile
|
|
10
|
+
* cannot deregister. So no cleanup here depends on the dying writer. Any
|
|
11
|
+
* replica that reads the roster sweeps it, every sweep write is conditional
|
|
12
|
+
* on the ETag it read, and a lost race is skipped, so concurrent sweepers
|
|
13
|
+
* converge. Registration waits until the writer is encountered in a second
|
|
14
|
+
* session under the same account, which keeps one-shot writers (an incognito
|
|
15
|
+
* window, a cleared profile) out of the roster altogether.
|
|
16
|
+
*/
|
|
17
|
+
import { NotSupportedError } from '@interop/was-client';
|
|
18
|
+
import { parseRegisteredWriterEntry, registeredWriterPolicy, registeredWriterResourceId } from './entry.js';
|
|
19
|
+
const PRECONDITION_FAILED_ERROR_NAME = 'PreconditionFailedError';
|
|
20
|
+
const NOT_FOUND_ERROR_NAME = 'NotFoundError';
|
|
21
|
+
const REGISTRATION_ATTEMPTS = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a store write lost a race: a conditional write refused, or (for a
|
|
24
|
+
* delete) the resource already gone. A put that meets `NotFoundError` has
|
|
25
|
+
* not raced. It is refused, so it propagates. Matched by name, since the
|
|
26
|
+
* store is an injected seam that may raise from a second copy of was-client.
|
|
27
|
+
*
|
|
28
|
+
* @param err {unknown}
|
|
29
|
+
* @param [options] {object}
|
|
30
|
+
* @param [options.deleting] {boolean}
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isLostRace(err, { deleting = false } = {}) {
|
|
34
|
+
const name = err?.name;
|
|
35
|
+
return (name === PRECONDITION_FAILED_ERROR_NAME ||
|
|
36
|
+
(deleting && name === NOT_FOUND_ERROR_NAME));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The ETag a guarded write over an existing resource pins to. A read that
|
|
40
|
+
* carried none would send the write unconditionally and could overwrite a
|
|
41
|
+
* newer touch or rename, so it is refused instead.
|
|
42
|
+
*
|
|
43
|
+
* @param options {object}
|
|
44
|
+
* @param options.resourceId {string}
|
|
45
|
+
* @param [options.etag] {string}
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function requireEtag({ resourceId, etag }) {
|
|
49
|
+
if (etag === undefined) {
|
|
50
|
+
throw new NotSupportedError(`Cannot update registered writer "${resourceId}": its read returned ` +
|
|
51
|
+
'no ETag, so the write would go out unconditionally. A browser ' +
|
|
52
|
+
"client needs `ETag` in the server's `Access-Control-Expose-Headers`.");
|
|
53
|
+
}
|
|
54
|
+
return etag;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* An entry's `lastSeen` as a time no later than `now`. A writer's clock
|
|
58
|
+
* that runs fast would otherwise keep its entry newest, ahead of every
|
|
59
|
+
* eviction.
|
|
60
|
+
*
|
|
61
|
+
* @param options {object}
|
|
62
|
+
* @param options.entry {RegisteredWriterEntry}
|
|
63
|
+
* @param options.now {Date}
|
|
64
|
+
* @returns {number}
|
|
65
|
+
*/
|
|
66
|
+
function effectiveLastSeen({ entry, now }) {
|
|
67
|
+
return Math.min(Date.parse(entry.lastSeen), now.getTime());
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reads the writer first-seen record, or `undefined` when it is missing or
|
|
71
|
+
* malformed.
|
|
72
|
+
*
|
|
73
|
+
* @param body {unknown}
|
|
74
|
+
* @returns {WriterFirstSeenRecord | undefined}
|
|
75
|
+
*/
|
|
76
|
+
function parseFirstSeenRecord(body) {
|
|
77
|
+
if (body === null || typeof body !== 'object') {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const { accountDid, writerId, firstSessionAt, lastTouchedAt } = body;
|
|
81
|
+
if (typeof accountDid !== 'string' ||
|
|
82
|
+
typeof writerId !== 'string' ||
|
|
83
|
+
typeof firstSessionAt !== 'string' ||
|
|
84
|
+
Number.isNaN(Date.parse(firstSessionAt)) ||
|
|
85
|
+
(lastTouchedAt !== undefined &&
|
|
86
|
+
(typeof lastTouchedAt !== 'string' ||
|
|
87
|
+
Number.isNaN(Date.parse(lastTouchedAt))))) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
accountDid,
|
|
92
|
+
writerId,
|
|
93
|
+
firstSessionAt,
|
|
94
|
+
...(lastTouchedAt !== undefined && { lastTouchedAt })
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One stored resource read as an entry of this account's roster: a
|
|
99
|
+
* well-formed entry stored at its own resource id. An entry stored under any
|
|
100
|
+
* other id is skipped, so a copied or misplaced entry labels nothing.
|
|
101
|
+
*
|
|
102
|
+
* @param options {object}
|
|
103
|
+
* @param options.accountDid {string}
|
|
104
|
+
* @param options.stored {StoredRegisteredWriter}
|
|
105
|
+
* @returns {RegisteredWriterEntry | undefined}
|
|
106
|
+
*/
|
|
107
|
+
function boundEntry({ accountDid, stored }) {
|
|
108
|
+
const entry = parseRegisteredWriterEntry(stored.body);
|
|
109
|
+
if (entry === undefined ||
|
|
110
|
+
registeredWriterResourceId({ accountDid, writerId: entry.writerId }) !==
|
|
111
|
+
stored.resourceId) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Registers this client's writer in the roster once it is encountered in a
|
|
118
|
+
* second session under the account, and touches its `lastSeen` at most once
|
|
119
|
+
* per touch interval after that. Call it on the wallet's connect or sync path;
|
|
120
|
+
* calls within the interval cost no I/O.
|
|
121
|
+
*
|
|
122
|
+
* A new entry takes `label`. A touch keeps the stored label, so a rename
|
|
123
|
+
* stands, and refreshes `signingKeyMultibase` when one is given. Registering
|
|
124
|
+
* a new entry first makes room under the entry cap, deleting the
|
|
125
|
+
* oldest-`lastSeen` entries.
|
|
126
|
+
*
|
|
127
|
+
* @param options {object}
|
|
128
|
+
* @param options.store {RegisteredWritersStore}
|
|
129
|
+
* @param options.local {WriterFirstSeenStore}
|
|
130
|
+
* @param options.accountDid {string} the account whose Space holds the roster
|
|
131
|
+
* @param options.writerId {string} this client's writer
|
|
132
|
+
* @param options.label {string} the coarse platform self-description a new
|
|
133
|
+
* entry takes
|
|
134
|
+
* @param options.sessionStartedAt {Date} when the current session began; a
|
|
135
|
+
* first-seen record written at or after it is this session's own
|
|
136
|
+
* @param [options.signingKeyMultibase] {string} an enrolled wallet client's
|
|
137
|
+
* own signing-key multibase
|
|
138
|
+
* @param [options.now] {Date}
|
|
139
|
+
* @param [options.policy] {Partial<RegisteredWriterPolicy>}
|
|
140
|
+
* @returns {Promise<WriterRegistrationOutcome>}
|
|
141
|
+
*/
|
|
142
|
+
export async function registerWriterOnSecondSession({ store, local, accountDid, writerId, label, sessionStartedAt, signingKeyMultibase, now = new Date(), policy }) {
|
|
143
|
+
const resolved = registeredWriterPolicy(policy);
|
|
144
|
+
const firstSeen = parseFirstSeenRecord(await local.get());
|
|
145
|
+
if (firstSeen === undefined ||
|
|
146
|
+
firstSeen.accountDid !== accountDid ||
|
|
147
|
+
firstSeen.writerId !== writerId) {
|
|
148
|
+
await local.put({
|
|
149
|
+
accountDid,
|
|
150
|
+
writerId,
|
|
151
|
+
firstSessionAt: sessionStartedAt.toISOString()
|
|
152
|
+
});
|
|
153
|
+
return 'deferred';
|
|
154
|
+
}
|
|
155
|
+
if (Date.parse(firstSeen.firstSessionAt) >= sessionStartedAt.getTime()) {
|
|
156
|
+
return 'deferred';
|
|
157
|
+
}
|
|
158
|
+
if (firstSeen.lastTouchedAt !== undefined &&
|
|
159
|
+
now.getTime() - Date.parse(firstSeen.lastTouchedAt) <
|
|
160
|
+
resolved.touchIntervalMs) {
|
|
161
|
+
return 'fresh';
|
|
162
|
+
}
|
|
163
|
+
const resourceId = registeredWriterResourceId({ accountDid, writerId });
|
|
164
|
+
const lastSeen = now.toISOString();
|
|
165
|
+
for (let attempt = 0; attempt < REGISTRATION_ATTEMPTS; attempt++) {
|
|
166
|
+
const stored = await store.get({ resourceId });
|
|
167
|
+
const existing = stored === undefined
|
|
168
|
+
? undefined
|
|
169
|
+
: boundEntry({ accountDid, stored: { resourceId, ...stored } });
|
|
170
|
+
const entry = {
|
|
171
|
+
version: 1,
|
|
172
|
+
writerId,
|
|
173
|
+
...(signingKeyMultibase !== undefined
|
|
174
|
+
? { signingKeyMultibase }
|
|
175
|
+
: existing?.signingKeyMultibase !== undefined && {
|
|
176
|
+
signingKeyMultibase: existing.signingKeyMultibase
|
|
177
|
+
}),
|
|
178
|
+
label: existing?.label ?? label,
|
|
179
|
+
lastSeen,
|
|
180
|
+
active: true
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
if (stored === undefined) {
|
|
184
|
+
await sweepStored({
|
|
185
|
+
store,
|
|
186
|
+
accountDid,
|
|
187
|
+
stored: await store.list(),
|
|
188
|
+
now,
|
|
189
|
+
policy: resolved,
|
|
190
|
+
capacity: resolved.maxEntries - 1
|
|
191
|
+
});
|
|
192
|
+
await store.put({ resourceId, entry, ifNoneMatch: true });
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
await store.put({
|
|
196
|
+
resourceId,
|
|
197
|
+
entry,
|
|
198
|
+
ifMatch: requireEtag({ resourceId, etag: stored.etag })
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (isLostRace(err)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
await local.put({ ...firstSeen, lastTouchedAt: lastSeen });
|
|
209
|
+
return existing === undefined ? 'registered' : 'touched';
|
|
210
|
+
}
|
|
211
|
+
return 'raced';
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Renames this client's writer. The rename lands only on a registered entry;
|
|
215
|
+
* before registration there is nothing to rename, and the call returns
|
|
216
|
+
* `false`.
|
|
217
|
+
*
|
|
218
|
+
* @param options {object}
|
|
219
|
+
* @param options.store {RegisteredWritersStore}
|
|
220
|
+
* @param options.accountDid {string}
|
|
221
|
+
* @param options.writerId {string}
|
|
222
|
+
* @param options.label {string} the new label; blank is refused
|
|
223
|
+
* @returns {Promise<boolean>} whether an entry was renamed
|
|
224
|
+
*/
|
|
225
|
+
export async function renameRegisteredWriter({ store, accountDid, writerId, label }) {
|
|
226
|
+
const trimmed = label.trim();
|
|
227
|
+
if (!trimmed) {
|
|
228
|
+
throw new TypeError('A writer label must not be blank.');
|
|
229
|
+
}
|
|
230
|
+
const resourceId = registeredWriterResourceId({ accountDid, writerId });
|
|
231
|
+
const stored = await store.get({ resourceId });
|
|
232
|
+
if (stored === undefined) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const existing = boundEntry({ accountDid, stored: { resourceId, ...stored } });
|
|
236
|
+
if (existing === undefined) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
await store.put({
|
|
240
|
+
resourceId,
|
|
241
|
+
entry: { ...existing, label: trimmed },
|
|
242
|
+
ifMatch: requireEtag({ resourceId, etag: stored.etag })
|
|
243
|
+
});
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The two-phase expiry over a listing already read. An active entry past the
|
|
248
|
+
* inactivity TTL is marked inactive. Past `capacity` entries, the
|
|
249
|
+
* oldest-`lastSeen` ones are deleted (ties broken by resource id, so every
|
|
250
|
+
* sweeper picks the same ones). A resource that is not a bound entry is left
|
|
251
|
+
* alone and not counted: it may be an entry sealed under an epoch this reader
|
|
252
|
+
* does not hold yet.
|
|
253
|
+
*
|
|
254
|
+
* @param options {object}
|
|
255
|
+
* @param options.store {RegisteredWritersStore}
|
|
256
|
+
* @param options.accountDid {string}
|
|
257
|
+
* @param options.stored {StoredRegisteredWriter[]}
|
|
258
|
+
* @param options.now {Date}
|
|
259
|
+
* @param options.policy {RegisteredWriterPolicy}
|
|
260
|
+
* @param options.capacity {number} the most entries to leave standing
|
|
261
|
+
* @returns {Promise<RegisteredWritersSweep>}
|
|
262
|
+
*/
|
|
263
|
+
async function sweepStored({ store, accountDid, stored, now, policy, capacity }) {
|
|
264
|
+
const bound = [];
|
|
265
|
+
for (const item of stored) {
|
|
266
|
+
const entry = boundEntry({ accountDid, stored: item });
|
|
267
|
+
if (entry !== undefined) {
|
|
268
|
+
bound.push({ stored: item, entry });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
bound.sort((left, right) => effectiveLastSeen({ entry: right.entry, now }) -
|
|
272
|
+
effectiveLastSeen({ entry: left.entry, now }) ||
|
|
273
|
+
(left.stored.resourceId < right.stored.resourceId ? -1 : 1));
|
|
274
|
+
const markedInactive = [];
|
|
275
|
+
const deleted = [];
|
|
276
|
+
const entries = [];
|
|
277
|
+
for (const [index, { stored: item, entry }] of bound.entries()) {
|
|
278
|
+
// A resource read without an ETag is never written: an unconditional
|
|
279
|
+
// write could clobber a newer touch.
|
|
280
|
+
if (index >= Math.max(capacity, 0)) {
|
|
281
|
+
if (item.etag === undefined) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
await store.delete({ resourceId: item.resourceId, ifMatch: item.etag });
|
|
286
|
+
deleted.push(entry.writerId);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (!isLostRace(err, { deleting: true })) {
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const expired = now.getTime() - effectiveLastSeen({ entry, now }) > policy.inactiveAfterMs;
|
|
296
|
+
if (entry.active && expired && item.etag !== undefined) {
|
|
297
|
+
const inactive = { ...entry, active: false };
|
|
298
|
+
try {
|
|
299
|
+
await store.put({
|
|
300
|
+
resourceId: item.resourceId,
|
|
301
|
+
entry: inactive,
|
|
302
|
+
ifMatch: item.etag
|
|
303
|
+
});
|
|
304
|
+
markedInactive.push(entry.writerId);
|
|
305
|
+
entries.push(inactive);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (!isLostRace(err)) {
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
// A newer write won; the entry as read still labels its revisions.
|
|
312
|
+
entries.push(entry);
|
|
313
|
+
}
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
entries.push(entry);
|
|
317
|
+
}
|
|
318
|
+
return { entries, markedInactive, deleted };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Reads the roster and sweeps it: the sweep-on-read every history view runs.
|
|
322
|
+
* Safe to run from any replica, and concurrently.
|
|
323
|
+
*
|
|
324
|
+
* @param options {object}
|
|
325
|
+
* @param options.store {RegisteredWritersStore}
|
|
326
|
+
* @param options.accountDid {string} the account whose Space holds the roster
|
|
327
|
+
* @param [options.now] {Date}
|
|
328
|
+
* @param [options.policy] {Partial<RegisteredWriterPolicy>}
|
|
329
|
+
* @returns {Promise<RegisteredWritersSweep>}
|
|
330
|
+
*/
|
|
331
|
+
export async function sweepRegisteredWriters({ store, accountDid, now = new Date(), policy }) {
|
|
332
|
+
const resolved = registeredWriterPolicy(policy);
|
|
333
|
+
return sweepStored({
|
|
334
|
+
store,
|
|
335
|
+
accountDid,
|
|
336
|
+
stored: await store.list(),
|
|
337
|
+
now,
|
|
338
|
+
policy: resolved,
|
|
339
|
+
capacity: resolved.maxEntries
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolves a revision's `writerId` against the swept roster.
|
|
344
|
+
*
|
|
345
|
+
* @param options {object}
|
|
346
|
+
* @param options.writerId {string}
|
|
347
|
+
* @param options.entries {RegisteredWriterEntry[]} a sweep's `entries`
|
|
348
|
+
* @param options.enrolledSigningKeys {ReadonlySet<string>} the signing-key
|
|
349
|
+
* multibases of the enrolled clients in the verified account document
|
|
350
|
+
* @returns {ResolvedWriter | undefined}
|
|
351
|
+
*/
|
|
352
|
+
export function resolveRegisteredWriter({ writerId, entries, enrolledSigningKeys }) {
|
|
353
|
+
const entry = entries.find(candidate => candidate.writerId === writerId);
|
|
354
|
+
if (entry === undefined) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
const linked = entry.signingKeyMultibase !== undefined &&
|
|
358
|
+
enrolledSigningKeys.has(entry.signingKeyMultibase);
|
|
359
|
+
return {
|
|
360
|
+
label: entry.label,
|
|
361
|
+
active: entry.active,
|
|
362
|
+
...(linked && { clientSigningKeyMultibase: entry.signingKeyMultibase })
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
//# sourceMappingURL=roster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster.js","sourceRoot":"","sources":["../../src/writers/roster.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,YAAY,CAAA;AA+CnB,MAAM,8BAA8B,GAAG,yBAAyB,CAAA;AAChE,MAAM,oBAAoB,GAAG,eAAe,CAAA;AAC5C,MAAM,qBAAqB,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;;GAUG;AACH,SAAS,UAAU,CACjB,GAAY,EACZ,EAAE,QAAQ,GAAG,KAAK,KAA6B,EAAE;IAEjD,MAAM,IAAI,GAAI,GAAiC,EAAE,IAAI,CAAA;IACrD,OAAO,CACL,IAAI,KAAK,8BAA8B;QACvC,CAAC,QAAQ,IAAI,IAAI,KAAK,oBAAoB,CAAC,CAC5C,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,WAAW,CAAC,EACnB,UAAU,EACV,IAAI,EAIL;IACC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,iBAAiB,CACzB,oCAAoC,UAAU,uBAAuB;YACnE,gEAAgE;YAChE,sEAAsE,CACzE,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,EACzB,KAAK,EACL,GAAG,EAIJ;IACC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,SAAS,oBAAoB,CAC3B,IAAa;IAEb,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,GAC3D,IAA+B,CAAA;IACjC,IACE,OAAO,UAAU,KAAK,QAAQ;QAC9B,OAAO,QAAQ,KAAK,QAAQ;QAC5B,OAAO,cAAc,KAAK,QAAQ;QAClC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACxC,CAAC,aAAa,KAAK,SAAS;YAC1B,CAAC,OAAO,aAAa,KAAK,QAAQ;gBAChC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAC7C,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO;QACL,UAAU;QACV,QAAQ;QACR,cAAc;QACd,GAAG,CAAC,aAAa,KAAK,SAAS,IAAI,EAAE,aAAa,EAAE,CAAC;KACtD,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,UAAU,CAAC,EAClB,UAAU,EACV,MAAM,EAIP;IACC,MAAM,KAAK,GAAG,0BAA0B,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACrD,IACE,KAAK,KAAK,SAAS;QACnB,0BAA0B,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;YAClE,MAAM,CAAC,UAAU,EACnB,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,EAClD,KAAK,EACL,KAAK,EACL,UAAU,EACV,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,mBAAmB,EACnB,GAAG,GAAG,IAAI,IAAI,EAAE,EAChB,MAAM,EAWP;IACC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAA;IAC/C,MAAM,SAAS,GAAG,oBAAoB,CAAC,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC,CAAA;IACzD,IACE,SAAS,KAAK,SAAS;QACvB,SAAS,CAAC,UAAU,KAAK,UAAU;QACnC,SAAS,CAAC,QAAQ,KAAK,QAAQ,EAC/B,CAAC;QACD,MAAM,KAAK,CAAC,GAAG,CAAC;YACd,UAAU;YACV,QAAQ;YACR,cAAc,EAAE,gBAAgB,CAAC,WAAW,EAAE;SAC/C,CAAC,CAAA;QACF,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,gBAAgB,CAAC,OAAO,EAAE,EAAE,CAAC;QACvE,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,IACE,SAAS,CAAC,aAAa,KAAK,SAAS;QACrC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,aAAa,CAAC;YACjD,QAAQ,CAAC,eAAe,EAC1B,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,MAAM,UAAU,GAAG,0BAA0B,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAA;IACvE,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAA;IAClC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,qBAAqB,EAAE,OAAO,EAAE,EAAE,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAA;QAC9C,MAAM,QAAQ,GACZ,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC,CAAA;QACnE,MAAM,KAAK,GAA0B;YACnC,OAAO,EAAE,CAAC;YACV,QAAQ;YACR,GAAG,CAAC,mBAAmB,KAAK,SAAS;gBACnC,CAAC,CAAC,EAAE,mBAAmB,EAAE;gBACzB,CAAC,CAAC,QAAQ,EAAE,mBAAmB,KAAK,SAAS,IAAI;oBAC7C,mBAAmB,EAAE,QAAQ,CAAC,mBAAmB;iBAClD,CAAC;YACN,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,KAAK;YAC/B,QAAQ;YACR,MAAM,EAAE,IAAI;SACb,CAAA;QACD,IAAI,CAAC;YACH,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,WAAW,CAAC;oBAChB,KAAK;oBACL,UAAU;oBACV,MAAM,EAAE,MAAM,KAAK,CAAC,IAAI,EAAE;oBAC1B,GAAG;oBACH,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,CAAC;iBAClC,CAAC,CAAA;gBACF,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3D,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAC,GAAG,CAAC;oBACd,UAAU;oBACV,KAAK;oBACL,OAAO,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;iBACxD,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACpB,SAAQ;YACV,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;QACD,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC1D,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1D,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,QAAQ,EACR,KAAK,EAMN;IACC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAA;IAC1D,CAAC;IACD,MAAM,UAAU,GAAG,0BAA0B,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAA;IACvE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAA;IAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC,CAAA;IAC9E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,KAAK,CAAC,GAAG,CAAC;QACd,UAAU;QACV,KAAK,EAAE,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE;QACtC,OAAO,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;KACxD,CAAC,CAAA;IACF,OAAO,IAAI,CAAA;AACb,CAAC;AAWD;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,UAAU,WAAW,CAAC,EACzB,KAAK,EACL,UAAU,EACV,MAAM,EACN,GAAG,EACH,MAAM,EACN,QAAQ,EAQT;IACC,MAAM,KAAK,GAGN,EAAE,CAAA;IACP,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACtD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CACR,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACd,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QAC5C,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC9D,CAAA;IAED,MAAM,cAAc,GAAa,EAAE,CAAA;IACnC,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAA4B,EAAE,CAAA;IAC3C,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/D,qEAAqE;QACrE,qCAAqC;QACrC,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC;YACnC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC5B,SAAQ;YACV,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;gBACvE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;YAC9B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;oBACzC,MAAM,GAAG,CAAA;gBACX,CAAC;YACH,CAAC;YACD,SAAQ;QACV,CAAC;QACD,MAAM,OAAO,GACX,GAAG,CAAC,OAAO,EAAE,GAAG,iBAAiB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC,eAAe,CAAA;QAC5E,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACvD,MAAM,QAAQ,GAA0B,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;YACnE,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,GAAG,CAAC;oBACd,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,KAAK,EAAE,QAAQ;oBACf,OAAO,EAAE,IAAI,CAAC,IAAI;iBACnB,CAAC,CAAA;gBACF,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;gBACnC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACxB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACrB,MAAM,GAAG,CAAA;gBACX,CAAC;gBACD,mEAAmE;gBACnE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACrB,CAAC;YACD,SAAQ;QACV,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,CAAA;AAC7C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,GAAG,GAAG,IAAI,IAAI,EAAE,EAChB,MAAM,EAMP;IACC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAA;IAC/C,OAAO,WAAW,CAAC;QACjB,KAAK;QACL,UAAU;QACV,MAAM,EAAE,MAAM,KAAK,CAAC,IAAI,EAAE;QAC1B,GAAG;QACH,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,QAAQ,CAAC,UAAU;KAC9B,CAAC,CAAA;AACJ,CAAC;AAkBD;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,QAAQ,EACR,OAAO,EACP,mBAAmB,EAKpB;IACC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAA;IACxE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,MAAM,GACV,KAAK,CAAC,mBAAmB,KAAK,SAAS;QACvC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAA;IACpD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,MAAM,IAAI,EAAE,yBAAyB,EAAE,KAAK,CAAC,mBAAmB,EAAE,CAAC;KACxE,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The writer roster's storage seam, and its WAS-backed adapter over the
|
|
6
|
+
* `registered-writers` collection.
|
|
7
|
+
*
|
|
8
|
+
* One resource per writer keeps registration, liveness touches, and sweeps
|
|
9
|
+
* off any shared document. Every write the roster logic makes is conditional
|
|
10
|
+
* on the ETag it read, so two sweepers, or a sweeper and a touching writer,
|
|
11
|
+
* cannot overwrite each other's newer write. A lost race surfaces as an error
|
|
12
|
+
* named `PreconditionFailedError`, which the roster logic matches by name.
|
|
13
|
+
*/
|
|
14
|
+
import type { Collection } from '@interop/was-client';
|
|
15
|
+
import type { RegisteredWriterEntry } from './entry.js';
|
|
16
|
+
/**
|
|
17
|
+
* One stored resource: its body as read (`undefined` when it could not be
|
|
18
|
+
* read or opened) and its ETag.
|
|
19
|
+
*/
|
|
20
|
+
export interface StoredRegisteredWriter {
|
|
21
|
+
resourceId: string;
|
|
22
|
+
body: unknown;
|
|
23
|
+
etag?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The Space-side seam of the roster.
|
|
27
|
+
*/
|
|
28
|
+
export interface RegisteredWritersStore {
|
|
29
|
+
/**
|
|
30
|
+
* Every resource in the collection. A resource that fails to read or
|
|
31
|
+
* decrypt is listed with an `undefined` body rather than failing the list.
|
|
32
|
+
*/
|
|
33
|
+
list(): Promise<StoredRegisteredWriter[]>;
|
|
34
|
+
/**
|
|
35
|
+
* One resource, or `undefined` when it does not exist.
|
|
36
|
+
*/
|
|
37
|
+
get(options: {
|
|
38
|
+
resourceId: string;
|
|
39
|
+
}): Promise<Omit<StoredRegisteredWriter, 'resourceId'> | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Writes one entry: conditional on `ifMatch`, or create-only under
|
|
42
|
+
* `ifNoneMatch`.
|
|
43
|
+
*/
|
|
44
|
+
put(options: {
|
|
45
|
+
resourceId: string;
|
|
46
|
+
entry: RegisteredWriterEntry;
|
|
47
|
+
ifMatch?: string;
|
|
48
|
+
ifNoneMatch?: boolean;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Deletes one resource, conditional on `ifMatch` when given.
|
|
52
|
+
*/
|
|
53
|
+
delete(options: {
|
|
54
|
+
resourceId: string;
|
|
55
|
+
ifMatch?: string;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds the roster store over the `registered-writers` collection handle.
|
|
60
|
+
*
|
|
61
|
+
* @param options {object}
|
|
62
|
+
* @param options.collection {Collection} the account data Space's
|
|
63
|
+
* `registered-writers` collection, opened through a storage client whose
|
|
64
|
+
* encryption provider seals and opens it like any other encrypted wallet
|
|
65
|
+
* collection (and carrying the invocation capability, if any)
|
|
66
|
+
* @returns {RegisteredWritersStore}
|
|
67
|
+
*/
|
|
68
|
+
export declare function wasRegisteredWritersStore({ collection }: {
|
|
69
|
+
collection: Collection;
|
|
70
|
+
}): RegisteredWritersStore;
|
|
71
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/writers/store.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAA;IACzC;;OAEG;IACH,GAAG,CAAC,OAAO,EAAE;QACX,UAAU,EAAE,MAAM,CAAA;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC,CAAA;IACnE;;;OAGG;IACH,GAAG,CAAC,OAAO,EAAE;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,qBAAqB,CAAA;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjB;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACX,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;CACvB,GAAG,sBAAsB,CA+CzB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the roster store over the `registered-writers` collection handle.
|
|
3
|
+
*
|
|
4
|
+
* @param options {object}
|
|
5
|
+
* @param options.collection {Collection} the account data Space's
|
|
6
|
+
* `registered-writers` collection, opened through a storage client whose
|
|
7
|
+
* encryption provider seals and opens it like any other encrypted wallet
|
|
8
|
+
* collection (and carrying the invocation capability, if any)
|
|
9
|
+
* @returns {RegisteredWritersStore}
|
|
10
|
+
*/
|
|
11
|
+
export function wasRegisteredWritersStore({ collection }) {
|
|
12
|
+
async function read(resourceId) {
|
|
13
|
+
const result = await collection.resource(resourceId).getWithEtag();
|
|
14
|
+
if (result === null) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
body: result.data,
|
|
19
|
+
...(result.etag !== undefined && { etag: result.etag })
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async list() {
|
|
24
|
+
const ids = [];
|
|
25
|
+
for await (const item of collection.listItems()) {
|
|
26
|
+
ids.push(item.id);
|
|
27
|
+
}
|
|
28
|
+
return Promise.all(ids.map(async (resourceId) => {
|
|
29
|
+
try {
|
|
30
|
+
const stored = await read(resourceId);
|
|
31
|
+
return { resourceId, body: stored?.body, etag: stored?.etag };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { resourceId, body: undefined };
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
async get({ resourceId }) {
|
|
39
|
+
return read(resourceId);
|
|
40
|
+
},
|
|
41
|
+
async put({ resourceId, entry, ifMatch, ifNoneMatch }) {
|
|
42
|
+
await collection.put(resourceId, entry, {
|
|
43
|
+
contentType: 'application/json',
|
|
44
|
+
...(ifMatch !== undefined && { ifMatch }),
|
|
45
|
+
...(ifNoneMatch === true && { ifNoneMatch })
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
async delete({ resourceId, ifMatch }) {
|
|
49
|
+
await collection
|
|
50
|
+
.resource(resourceId)
|
|
51
|
+
.delete(ifMatch !== undefined ? { ifMatch } : {});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/writers/store.ts"],"names":[],"mappings":"AAyDA;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,EACxC,UAAU,EAGX;IACC,KAAK,UAAU,IAAI,CACjB,UAAkB;QAElB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAA;QAClE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;SACxD,CAAA;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI;YACR,MAAM,GAAG,GAAa,EAAE,CAAA;YACxB,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC;gBAChD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACnB,CAAC;YACD,OAAO,OAAO,CAAC,GAAG,CAChB,GAAG,CAAC,GAAG,CAAC,KAAK,EAAC,UAAU,EAAC,EAAE;gBACzB,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,CAAA;oBACrC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;gBAC/D,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;gBACxC,CAAC;YACH,CAAC,CAAC,CACH,CAAA;QACH,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE;YACtB,OAAO,IAAI,CAAC,UAAU,CAAC,CAAA;QACzB,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE;YACnD,MAAM,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,EAAE;gBACtC,WAAW,EAAE,kBAAkB;gBAC/B,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC;gBACzC,GAAG,CAAC,WAAW,KAAK,IAAI,IAAI,EAAE,WAAW,EAAE,CAAC;aAC7C,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE;YAClC,MAAM,UAAU;iBACb,QAAQ,CAAC,UAAU,CAAC;iBACpB,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACrD,CAAC;KACF,CAAA;AACH,CAAC"}
|