@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.
@@ -1,4 +1,4 @@
1
- import { a as findInvitation, c as invitationById, d as redeemInvitation, f as saveChallenge, h as userHandle, i as createUser, m as touchPasskey, o as findPasskey, p as takeChallenge, r as createSession, t as addPasskey, u as passkeysOf } from "./store-C2hiR0yd.mjs";
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
- /** Creates an entry at version 1 with its first revision, as a draft. */
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
- /** A new definition, if the version is still `expected`: `conflict` when it moved. */
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 };