@hollware/server 0.9.1 → 0.11.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/dist/{ceremonies-D5dIEbha.mjs → ceremonies-CI3DkOEL.mjs} +20 -6
- package/dist/index.d.mts +69 -4
- package/dist/index.mjs +1789 -486
- package/dist/{store-C2hiR0yd.mjs → store-DjA9s95l.mjs} +88 -6
- package/migrations/0013_site_events.sql +31 -0
- package/migrations/0014_entry_trash.sql +1 -0
- package/migrations/0015_site_changes_append_only.sql +28 -0
- package/migrations/0016_audit_log.sql +65 -0
- package/migrations/meta/0013_snapshot.json +2619 -0
- package/migrations/meta/0014_snapshot.json +2626 -0
- package/migrations/meta/0015_snapshot.json +2626 -0
- package/migrations/meta/0016_snapshot.json +2823 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +8 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as saveChallenge, b as userHandle, c as findInvitation, d as invitationById, h as redeemInvitation, i as createUser, l as findPasskey, m as passkeysOf, r as createSession, t as addPasskey, v as takeChallenge, y as touchPasskey } from "./store-DjA9s95l.mjs";
|
|
2
2
|
import { HollwareError, newId } from "@hollware/core";
|
|
3
3
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
4
4
|
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
|
@@ -15,6 +15,18 @@ function relyingParty(ceremony) {
|
|
|
15
15
|
function refused(message, fix) {
|
|
16
16
|
return new HollwareError("invalid_input", message, fix);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A verification of the library, or `{ verified: false }` when it throws: it throws for a response
|
|
20
|
+
* from another origin, without user verification, or with a counter that went back, and each of
|
|
21
|
+
* those is a passkey that could not be verified, not a failure of the server.
|
|
22
|
+
*/
|
|
23
|
+
async function unlessThrown(verify) {
|
|
24
|
+
try {
|
|
25
|
+
return await verify();
|
|
26
|
+
} catch {
|
|
27
|
+
return { verified: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
/** The options of `navigator.credentials.create` for the person of an invitation. */
|
|
19
31
|
async function registrationOptions(ceremony, invitationSecret) {
|
|
20
32
|
const invitation = await findInvitation(ceremony.db, invitationSecret, ceremony.now);
|
|
@@ -57,12 +69,13 @@ async function verifyRegistration(ceremony, input) {
|
|
|
57
69
|
});
|
|
58
70
|
if (challenge === null || challenge.invitationId === null || challenge.userId === null) throw refused("The registration expired.", "Start again from the link.");
|
|
59
71
|
const rp = relyingParty(ceremony);
|
|
60
|
-
const verified = await verifyRegistrationResponse({
|
|
72
|
+
const verified = await unlessThrown(() => verifyRegistrationResponse({
|
|
61
73
|
response: input.response,
|
|
62
74
|
expectedChallenge: challenge.challenge,
|
|
63
75
|
expectedOrigin: rp.origin,
|
|
64
|
-
expectedRPID: rp.id
|
|
65
|
-
|
|
76
|
+
expectedRPID: rp.id,
|
|
77
|
+
requireUserVerification: true
|
|
78
|
+
}));
|
|
66
79
|
if (!verified.verified) throw refused("The passkey could not be verified.", "Try again.");
|
|
67
80
|
const invitation = await invitationById(ceremony.db, challenge.invitationId);
|
|
68
81
|
if (invitation === null || !await redeemInvitation(ceremony.db, invitation.id, ceremony.now)) throw refused("This link is not valid: it was used already, or it expired.", "Ask the owner of this Hollware for a new invitation.");
|
|
@@ -122,18 +135,19 @@ async function verifyAuthentication(ceremony, input) {
|
|
|
122
135
|
const passkey = await findPasskey(ceremony.db, input.response.id);
|
|
123
136
|
if (passkey === null) throw refused("This passkey is not one of this Hollware.", "Use a passkey you registered here.");
|
|
124
137
|
const rp = relyingParty(ceremony);
|
|
125
|
-
const verified = await verifyAuthenticationResponse({
|
|
138
|
+
const verified = await unlessThrown(() => verifyAuthenticationResponse({
|
|
126
139
|
response: input.response,
|
|
127
140
|
expectedChallenge: challenge.challenge,
|
|
128
141
|
expectedOrigin: rp.origin,
|
|
129
142
|
expectedRPID: rp.id,
|
|
143
|
+
requireUserVerification: true,
|
|
130
144
|
credential: {
|
|
131
145
|
id: passkey.credentialId,
|
|
132
146
|
publicKey: isoBase64URL.toBuffer(passkey.publicKey),
|
|
133
147
|
counter: passkey.counter,
|
|
134
148
|
transports: JSON.parse(passkey.transports)
|
|
135
149
|
}
|
|
136
|
-
});
|
|
150
|
+
}));
|
|
137
151
|
if (!verified.verified) throw refused("The passkey could not be verified.", "Try again.");
|
|
138
152
|
await touchPasskey(ceremony.db, passkey.id, verified.authenticationInfo.newCounter, ceremony.now);
|
|
139
153
|
const session = await createSession(ceremony.db, {
|
package/dist/index.d.mts
CHANGED
|
@@ -33,6 +33,8 @@ interface StoredEntry {
|
|
|
33
33
|
readonly publishedAt: string | null;
|
|
34
34
|
/** When a scheduled entry will be published, or `null`. */
|
|
35
35
|
readonly scheduledAt: string | null;
|
|
36
|
+
/** When the entry went to the trash, or `null`. The reads leave a trashed entry out. */
|
|
37
|
+
readonly deletedAt: string | null;
|
|
36
38
|
readonly updatedAt: string;
|
|
37
39
|
}
|
|
38
40
|
/** A revision as the store holds it. */
|
|
@@ -99,6 +101,8 @@ interface ListQuery {
|
|
|
99
101
|
readonly locale?: string;
|
|
100
102
|
readonly tag?: string;
|
|
101
103
|
readonly status?: StoredEntry['status'];
|
|
104
|
+
/** The entries in the trash instead of the others; the published view has none. */
|
|
105
|
+
readonly trashed?: boolean;
|
|
102
106
|
readonly view: View;
|
|
103
107
|
readonly limit: number;
|
|
104
108
|
/** The `nextCursor` of the previous page. */
|
|
@@ -200,6 +204,15 @@ interface OutboxEvent {
|
|
|
200
204
|
readonly id: string;
|
|
201
205
|
readonly payload: string;
|
|
202
206
|
}
|
|
207
|
+
/** A new entry whose slug or translation an entry in the trash holds: restore that one. */
|
|
208
|
+
interface InTrash {
|
|
209
|
+
readonly outcome: 'in_trash';
|
|
210
|
+
readonly view: EntryView;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The reads of the store leave out an entry in the trash, in every view: for the operations it is
|
|
214
|
+
* not there until it is restored. Only `listEntries` with `trashed` reads the trash.
|
|
215
|
+
*/
|
|
203
216
|
interface ContentStore {
|
|
204
217
|
/** The entry with that collection, locale and slug, in the given view, or `null`. */
|
|
205
218
|
findEntry(site: string, ref: EntryRef, view: View): Promise<EntryView | null>;
|
|
@@ -228,14 +241,41 @@ interface ContentStore {
|
|
|
228
241
|
lookup(site: string): EntryLookup;
|
|
229
242
|
/** The latest publish or unpublish of an entry, or `null`. */
|
|
230
243
|
lastPublication(site: string, entryId: string): Promise<StoredPublication | null>;
|
|
231
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Creates an entry at version 1 with its first revision, as a draft. A slug or a translation
|
|
246
|
+
* that an entry in the trash holds is `in_trash`, by that entry.
|
|
247
|
+
*/
|
|
232
248
|
createEntry(site: string, entry: EntryRef & {
|
|
233
249
|
readonly translationGroup: string | null;
|
|
234
250
|
readonly revision: NewRevision;
|
|
235
251
|
}): Promise<{
|
|
236
252
|
readonly outcome: 'saved';
|
|
237
253
|
readonly view: EntryView;
|
|
238
|
-
} | WriteRefusal>;
|
|
254
|
+
} | WriteRefusal | InTrash>;
|
|
255
|
+
/**
|
|
256
|
+
* Moves a draft to the trash, if it is still at `expectedVersion`: the entry gets its
|
|
257
|
+
* `deletedAt` and the next version, and its pending review is cancelled in the same batch. No
|
|
258
|
+
* row is deleted. An entry that is published or scheduled is `not_draft`, and nothing is written.
|
|
259
|
+
*/
|
|
260
|
+
trashEntry(site: string, write: {
|
|
261
|
+
readonly entryId: string;
|
|
262
|
+
readonly expectedVersion: number;
|
|
263
|
+
}): Promise<{
|
|
264
|
+
readonly outcome: 'trashed' | 'not_draft';
|
|
265
|
+
readonly view: EntryView;
|
|
266
|
+
} | Extract<WriteRefusal, {
|
|
267
|
+
readonly outcome: 'conflict' | 'not_found';
|
|
268
|
+
}>>;
|
|
269
|
+
/** Brings an entry back from the trash as the draft it was, at the next version. */
|
|
270
|
+
restoreEntry(site: string, entryId: string): Promise<{
|
|
271
|
+
readonly outcome: 'restored';
|
|
272
|
+
readonly view: EntryView;
|
|
273
|
+
} | {
|
|
274
|
+
readonly outcome: 'not_trashed';
|
|
275
|
+
readonly view: EntryView;
|
|
276
|
+
} | {
|
|
277
|
+
readonly outcome: 'not_found';
|
|
278
|
+
}>;
|
|
239
279
|
/**
|
|
240
280
|
* Appends a revision and makes it the head, if the entry is still at `expectedVersion`. In
|
|
241
281
|
* one batch: the revision, its tags (only where the revision was inserted), the cancellation of
|
|
@@ -441,12 +481,16 @@ interface StructureActor {
|
|
|
441
481
|
*/
|
|
442
482
|
interface StructureStore {
|
|
443
483
|
read(site: string): Promise<StoredStructure | null>;
|
|
444
|
-
/**
|
|
484
|
+
/**
|
|
485
|
+
* A new definition, if the version is still `expected`: `conflict` when it moved. Its `event`,
|
|
486
|
+
* a `site.changed`, goes to the outbox in the same batch, and only when the definition does.
|
|
487
|
+
*/
|
|
445
488
|
write(input: {
|
|
446
489
|
readonly site: string;
|
|
447
490
|
readonly expected: number;
|
|
448
491
|
readonly definition: unknown;
|
|
449
492
|
readonly actor: StructureActor;
|
|
493
|
+
readonly event?: OutboxEvent;
|
|
450
494
|
}): Promise<'written' | 'conflict'>;
|
|
451
495
|
/** A new site with its definition, and its maker as owner: `taken` when the key exists, trashed or not. */
|
|
452
496
|
create(input: {
|
|
@@ -619,6 +663,13 @@ export interface HollwareEnv {
|
|
|
619
663
|
* Personal tokens work without it; OAuth does not.
|
|
620
664
|
*/
|
|
621
665
|
readonly OAUTH_KV: KVNamespace;
|
|
666
|
+
/**
|
|
667
|
+
* A Workers Rate Limiting binding (`ratelimits` in wrangler.jsonc): with it, the routes anyone
|
|
668
|
+
* may call without a token (sign-in and passkeys, OAuth's token and registration, the beacon)
|
|
669
|
+
* answer 429 past its limit, per address and route (T-10 of docs/security/threat-model.md).
|
|
670
|
+
* Without it, nothing is limited.
|
|
671
|
+
*/
|
|
672
|
+
readonly HOLLWARE_RATE_LIMIT?: RateLimiter;
|
|
622
673
|
/** The static assets of the Worker, which hold the admin at `/admin/`; without them, no admin. */
|
|
623
674
|
readonly ASSETS?: Fetcher;
|
|
624
675
|
/** The Queue of publication events (ADR-025); without it, the cron trigger delivers them. */
|
|
@@ -662,6 +713,12 @@ export interface HollwareRpc {
|
|
|
662
713
|
readonly formats?: readonly ('markdown' | 'document' | 'html')[];
|
|
663
714
|
/** Also the published entries closest by meaning (ADR-030). */
|
|
664
715
|
readonly related?: boolean;
|
|
716
|
+
/**
|
|
717
|
+
* A preview token (`hw_prv_…`, made in the admin): the entry it names is read in its latest
|
|
718
|
+
* revision, a draft included. A token that does not hold (expired, of another site or
|
|
719
|
+
* entry, forged) is an `invalid_input` error, never the published version in its place.
|
|
720
|
+
*/
|
|
721
|
+
readonly preview?: string;
|
|
665
722
|
}): Promise<Output<typeof getEntry>>;
|
|
666
723
|
listEntries(site: string, collection: string, options?: {
|
|
667
724
|
readonly locale?: string;
|
|
@@ -706,5 +763,13 @@ export type CreateHollware = (config?: HollwareConfig) => HollwareWorker;
|
|
|
706
763
|
* The config is resolved once, here; a broken one throws its `ConfigError`.
|
|
707
764
|
*/
|
|
708
765
|
export declare const createHollware: CreateHollware;
|
|
766
|
+
/** What a Workers Rate Limiting binding answers: whether one more request for `key` fits. */
|
|
767
|
+
export interface RateLimiter {
|
|
768
|
+
limit(options: {
|
|
769
|
+
readonly key: string;
|
|
770
|
+
}): Promise<{
|
|
771
|
+
readonly success: boolean;
|
|
772
|
+
}>;
|
|
773
|
+
}
|
|
709
774
|
//#endregion
|
|
710
|
-
export type { Actor, AnalyticsReader, AnalyticsWriter, Clock, ContentStore, Embedder, EntrySummaryView, EntryView, EventMessage, Handlers, ListQuery, NewRevision, OperationContext, OperationHandler, OutboxEvent, PageCount, PageView, Principal, PublishWrite, RelatedEntry, ReviewStatus, RevisionSummary, SearchHit, SearchQuery, Semantic, StoredEntry, StoredPublication, StoredReview, StoredReviewComment, StoredRevision, StoredSchedule, StoredStructure, StoredVector, Structure, StructureActor, StructureStore, VectorIndex, VectorMatch, VectorMetadata, View, WriteRefusal };
|
|
775
|
+
export type { Actor, AnalyticsReader, AnalyticsWriter, Clock, ContentStore, Embedder, EntrySummaryView, EntryView, EventMessage, Handlers, InTrash, ListQuery, NewRevision, OperationContext, OperationHandler, OutboxEvent, PageCount, PageView, Principal, PublishWrite, RelatedEntry, ReviewStatus, RevisionSummary, SearchHit, SearchQuery, Semantic, StoredEntry, StoredPublication, StoredReview, StoredReviewComment, StoredRevision, StoredSchedule, StoredStructure, StoredVector, Structure, StructureActor, StructureStore, VectorIndex, VectorMatch, VectorMetadata, View, WriteRefusal };
|