@hollware/core 0.10.0 → 1.0.0-rc.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/index.d.mts +146 -3
- package/dist/index.mjs +342 -60
- package/package.json +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -315,11 +315,18 @@ interface SiteDefinition {
|
|
|
315
315
|
/** The Worker secret that signs; Hollware's own sealed one when it is left out. */
|
|
316
316
|
readonly secret?: string;
|
|
317
317
|
}[];
|
|
318
|
-
/** Tells IndexNow the URLs that changed; `key`
|
|
318
|
+
/** Tells IndexNow the URLs that changed; `key` is the key itself, which is public. */
|
|
319
319
|
readonly indexNow?: {
|
|
320
320
|
readonly key: string;
|
|
321
321
|
};
|
|
322
322
|
};
|
|
323
|
+
/** What the site's activity log keeps (ADR-037): no addresses, and forever, by default. */
|
|
324
|
+
readonly audit?: {
|
|
325
|
+
/** `hashed` keeps a keyed hash of the address of each request; `off` keeps nothing of it. */
|
|
326
|
+
readonly ipAddresses?: 'off' | 'hashed';
|
|
327
|
+
/** How many days a line is kept, 30 to 3650; `null` keeps the log as long as the site. */
|
|
328
|
+
readonly retentionDays?: number | null;
|
|
329
|
+
};
|
|
323
330
|
}
|
|
324
331
|
/** What `hollware.config.ts` exports. */
|
|
325
332
|
interface HollwareConfig {
|
|
@@ -369,6 +376,12 @@ interface ResolvedSite {
|
|
|
369
376
|
readonly origins: readonly string[];
|
|
370
377
|
};
|
|
371
378
|
readonly events: ResolvedEvents;
|
|
379
|
+
readonly audit: ResolvedAudit;
|
|
380
|
+
}
|
|
381
|
+
/** What the activity log of a site keeps (ADR-037). */
|
|
382
|
+
interface ResolvedAudit {
|
|
383
|
+
readonly ipAddresses: 'off' | 'hashed';
|
|
384
|
+
readonly retentionDays: number | null;
|
|
372
385
|
}
|
|
373
386
|
/**
|
|
374
387
|
* A webhook of a site. `secret` names the Worker secret that signs its deliveries; `null` when the
|
|
@@ -450,6 +463,14 @@ export declare class ConfigError extends Error {
|
|
|
450
463
|
*/
|
|
451
464
|
export declare const INDEXNOW_KEY: RegExp;
|
|
452
465
|
//#endregion
|
|
466
|
+
//#region src/config/audit.d.ts
|
|
467
|
+
/** The fewest days a site may keep its activity log for, when it sets a retention (ADR-037). */
|
|
468
|
+
export declare const AUDIT_RETENTION_MIN_DAYS = 30;
|
|
469
|
+
/** The most days a retention may be: ten years. Beyond that, keep it with no retention. */
|
|
470
|
+
export declare const AUDIT_RETENTION_MAX_DAYS = 3650;
|
|
471
|
+
/** What a site that says nothing keeps: no addresses, and the log for as long as the site exists. */
|
|
472
|
+
export declare const DEFAULT_AUDIT: ResolvedAudit;
|
|
473
|
+
//#endregion
|
|
453
474
|
//#region src/config/constants.d.ts
|
|
454
475
|
/** The largest Markdown file a collection accepts when its config does not say, 512 KiB. */
|
|
455
476
|
export declare const DEFAULT_MAX_MARKDOWN_BYTES = 524288;
|
|
@@ -1124,6 +1145,34 @@ export declare const SITE_KEY_MAX_LENGTH = 64;
|
|
|
1124
1145
|
/** Whether a value is a site key: a string that fits `SITE_KEY_PATTERN` and is not too long. */
|
|
1125
1146
|
export declare function isSiteKey(value: unknown): value is string;
|
|
1126
1147
|
//#endregion
|
|
1148
|
+
//#region src/audit/index.d.ts
|
|
1149
|
+
/**
|
|
1150
|
+
* The actions of the activity log (ADR-037). A line names one of these, and nothing else: a new
|
|
1151
|
+
* operation or route that changes something is not logged until it is named here.
|
|
1152
|
+
*/
|
|
1153
|
+
/** Every operation that is not a read: each call of one is a line, whatever its outcome. */
|
|
1154
|
+
export declare const OPERATION_AUDIT_ACTIONS: readonly ['create_draft', 'update_draft', 'publish', 'unpublish', 'delete_entry', 'restore_entry', 'approve_review', 'request_review', 'comment_review', 'request_changes', 'update_site', 'set_collection', 'create_site', 'delete_site', 'restore_site', 'make_webhook_secret'];
|
|
1155
|
+
/** What people do outside the operations: their tokens, members, passkeys, sessions and apps. */
|
|
1156
|
+
export declare const IDENTITY_AUDIT_ACTIONS: readonly ['token.create',
|
|
1157
|
+
/** A person asked for a link that shows an entry's latest revision on the site (T-103). */
|
|
1158
|
+
'preview.create', 'token.revoke', 'member.role', 'member.remove', 'invitation.create', 'invitation.accept', 'session.sign_in', 'session.sign_out', 'session.end_all', 'passkey.remove', 'app.allow', 'app.revoke',
|
|
1159
|
+
/** A refresh token was used again after it was spent, so its grant was revoked (T-34). */
|
|
1160
|
+
'app.refresh_reused',
|
|
1161
|
+
/** The cron published an entry whose time had come, for the person who scheduled it. */
|
|
1162
|
+
'schedule.publish'];
|
|
1163
|
+
export declare const AUDIT_ACTIONS: readonly ["create_draft", "update_draft", "publish", "unpublish", "delete_entry", "restore_entry", "approve_review", "request_review", "comment_review", "request_changes", "update_site", "set_collection", "create_site", "delete_site", "restore_site", "make_webhook_secret", "token.create", "preview.create", "token.revoke", "member.role", "member.remove", "invitation.create", "invitation.accept", "session.sign_in", "session.sign_out", "session.end_all", "passkey.remove", "app.allow", "app.revoke", "app.refresh_reused", "schedule.publish"];
|
|
1164
|
+
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
|
1165
|
+
/** `denied`: a scope or a role was missing; `failed`: any other error the caller got. */
|
|
1166
|
+
export declare const AUDIT_OUTCOMES: readonly ['ok', 'denied', 'failed'];
|
|
1167
|
+
export type AuditOutcome = (typeof AUDIT_OUTCOMES)[number];
|
|
1168
|
+
/**
|
|
1169
|
+
* Who acted: a person in the admin, a personal token, an app a person allowed through OAuth, or
|
|
1170
|
+
* the deployment itself (its cron).
|
|
1171
|
+
*/
|
|
1172
|
+
export declare const AUDIT_ACTOR_KINDS: readonly ['user', 'token', 'oauth_client', 'system'];
|
|
1173
|
+
export type AuditActorKind = (typeof AUDIT_ACTOR_KINDS)[number];
|
|
1174
|
+
export declare function isAuditAction(value: string): value is AuditAction;
|
|
1175
|
+
//#endregion
|
|
1127
1176
|
//#region src/structure/compile.d.ts
|
|
1128
1177
|
/** The definitions of the sites, read from D1 as JSON, checked and resolved. */
|
|
1129
1178
|
export declare function compileSites(definitions: Readonly<Record<string, unknown>>): ResolvedConfig;
|
|
@@ -1189,6 +1238,13 @@ export declare const siteDefinitionData: z.ZodObject<{
|
|
|
1189
1238
|
key: z.ZodString;
|
|
1190
1239
|
}, z.core.$strict>>>;
|
|
1191
1240
|
}, z.core.$strict>>;
|
|
1241
|
+
audit: z.ZodDefault<z.ZodObject<{
|
|
1242
|
+
ipAddresses: z.ZodDefault<z.ZodEnum<{
|
|
1243
|
+
hashed: "hashed";
|
|
1244
|
+
off: "off";
|
|
1245
|
+
}>>;
|
|
1246
|
+
retentionDays: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
1247
|
+
}, z.core.$strict>>;
|
|
1192
1248
|
collections: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1193
1249
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
1194
1250
|
routes: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
@@ -1263,6 +1319,8 @@ export declare const ERROR_CODES: readonly [
|
|
|
1263
1319
|
'unknown_collection',
|
|
1264
1320
|
/** The input names a locale the site does not have. */
|
|
1265
1321
|
'unknown_locale',
|
|
1322
|
+
/** A preview link that has expired, or is not one Hollware made for this site (T-103). */
|
|
1323
|
+
'invalid_preview',
|
|
1266
1324
|
/** The token lacks a scope the operation requires; the message names every missing scope. */
|
|
1267
1325
|
'insufficient_scope',
|
|
1268
1326
|
/** The answer would exceed the output cap. Hollware never truncates content. */
|
|
@@ -1293,6 +1351,7 @@ export declare class HollwareError extends Error {
|
|
|
1293
1351
|
*/
|
|
1294
1352
|
/** The kinds of ids and their prefixes. A new kind adds its prefix here. */
|
|
1295
1353
|
export declare const ID_PREFIXES: {
|
|
1354
|
+
readonly auditLine: 'aud';
|
|
1296
1355
|
readonly challenge: 'chl';
|
|
1297
1356
|
readonly delivery: 'dlv';
|
|
1298
1357
|
readonly embedding: 'emb';
|
|
@@ -2574,6 +2633,13 @@ export declare const OPERATIONS: readonly [OperationSpec<"approve_review", z.Zod
|
|
|
2574
2633
|
key: z.ZodString;
|
|
2575
2634
|
}, z.core.$strict>>>;
|
|
2576
2635
|
}, z.core.$strict>>;
|
|
2636
|
+
audit: z.ZodDefault<z.ZodObject<{
|
|
2637
|
+
ipAddresses: z.ZodDefault<z.ZodEnum<{
|
|
2638
|
+
hashed: "hashed";
|
|
2639
|
+
off: "off";
|
|
2640
|
+
}>>;
|
|
2641
|
+
retentionDays: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
2642
|
+
}, z.core.$strict>>;
|
|
2577
2643
|
collections: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
2578
2644
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
2579
2645
|
routes: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
@@ -2849,6 +2915,13 @@ export declare const OPERATIONS: readonly [OperationSpec<"approve_review", z.Zod
|
|
|
2849
2915
|
key: z.ZodString;
|
|
2850
2916
|
}, z.core.$strict>>>;
|
|
2851
2917
|
}, z.core.$strict>>;
|
|
2918
|
+
audit: z.ZodDefault<z.ZodObject<{
|
|
2919
|
+
ipAddresses: z.ZodDefault<z.ZodEnum<{
|
|
2920
|
+
hashed: "hashed";
|
|
2921
|
+
off: "off";
|
|
2922
|
+
}>>;
|
|
2923
|
+
retentionDays: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
2924
|
+
}, z.core.$strict>>;
|
|
2852
2925
|
collections: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
2853
2926
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
2854
2927
|
routes: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
@@ -3463,6 +3536,13 @@ export declare const OPERATIONS: readonly [OperationSpec<"approve_review", z.Zod
|
|
|
3463
3536
|
key: z.ZodString;
|
|
3464
3537
|
}, z.core.$strict>>>;
|
|
3465
3538
|
}, z.core.$strict>>;
|
|
3539
|
+
audit: z.ZodDefault<z.ZodObject<{
|
|
3540
|
+
ipAddresses: z.ZodDefault<z.ZodEnum<{
|
|
3541
|
+
hashed: "hashed";
|
|
3542
|
+
off: "off";
|
|
3543
|
+
}>>;
|
|
3544
|
+
retentionDays: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
3545
|
+
}, z.core.$strict>>;
|
|
3466
3546
|
collections: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
3467
3547
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
3468
3548
|
routes: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
@@ -3615,13 +3695,76 @@ export interface WebhookCheck {
|
|
|
3615
3695
|
/** Now, in seconds since the epoch; the system clock when absent. */
|
|
3616
3696
|
readonly now?: number;
|
|
3617
3697
|
readonly toleranceSeconds?: number;
|
|
3698
|
+
/**
|
|
3699
|
+
* Whether the receiver sees this event for the first time, given its id, and records it (T-67):
|
|
3700
|
+
* a request captured and sent again within the tolerance is then refused. The id is read from
|
|
3701
|
+
* the signed body, never from `hollware-event-id`, which no signature covers. A retry of a
|
|
3702
|
+
* delivery carries the same id, so a receiver that records an id only once it has handled the
|
|
3703
|
+
* event lets a failed delivery be retried. Keep each id for longer than the tolerance.
|
|
3704
|
+
*/
|
|
3705
|
+
readonly isNew?: (eventId: string) => boolean | Promise<boolean>;
|
|
3618
3706
|
}
|
|
3619
3707
|
/**
|
|
3620
3708
|
* Whether a request is a webhook of Hollware: its signature matches the body and the secret, and
|
|
3621
|
-
* its timestamp is within the tolerance, so a captured request cannot be replayed later
|
|
3709
|
+
* its timestamp is within the tolerance, so a captured request cannot be replayed later; with
|
|
3710
|
+
* `isNew`, also that its event was not received before, so it cannot be replayed sooner either.
|
|
3622
3711
|
*/
|
|
3623
3712
|
export declare function verifyWebhook(check: WebhookCheck): Promise<boolean>;
|
|
3624
3713
|
//#endregion
|
|
3714
|
+
//#region src/preview.d.ts
|
|
3715
|
+
/**
|
|
3716
|
+
* The preview token (T-103 of docs/security/threat-model.md): a signed, stateless proof that a person of a site may read the
|
|
3717
|
+
* latest revision of one entry, for one hour. A site that reads published content only (the
|
|
3718
|
+
* binding of RPC, the read API) reads that entry's head instead when it presents one.
|
|
3719
|
+
*
|
|
3720
|
+
* hw_prv_<payload>.<signature>
|
|
3721
|
+
*
|
|
3722
|
+
* The payload is base64url of `{ "s": site, "e": entryId, "x": expiresAtSeconds }`, and the
|
|
3723
|
+
* signature the base64url HMAC-SHA256 of the payload text under a key that HKDF-SHA256 derives
|
|
3724
|
+
* from the Worker secret `HOLLWARE_SECRET`, bound to the site. Nothing is stored: a token cannot
|
|
3725
|
+
* be revoked, only wait out its hour, and rotating the secret ends every one.
|
|
3726
|
+
*/
|
|
3727
|
+
export declare const PREVIEW_TOKEN_PREFIX = "hw_prv_";
|
|
3728
|
+
/** The query parameter a site's page takes the token from. */
|
|
3729
|
+
export declare const PREVIEW_QUERY_PARAM = "hollware-preview";
|
|
3730
|
+
/** How long a token lasts, in seconds. */
|
|
3731
|
+
export declare const PREVIEW_TTL_SECONDS = 3600;
|
|
3732
|
+
/** The least characters of `HOLLWARE_SECRET` that Hollware signs anything with. */
|
|
3733
|
+
export declare const PREVIEW_SECRET_MIN_LENGTH = 32;
|
|
3734
|
+
/** Why a token was not accepted: for the log of the operator, never for the caller. */
|
|
3735
|
+
type PreviewRefusal = 'no_secret' | 'malformed' | 'signature' | 'site' | 'expired';
|
|
3736
|
+
type PreviewCheck = {
|
|
3737
|
+
readonly ok: true;
|
|
3738
|
+
readonly entryId: string;
|
|
3739
|
+
readonly expiresAt: string;
|
|
3740
|
+
} | {
|
|
3741
|
+
readonly ok: false;
|
|
3742
|
+
readonly reason: PreviewRefusal;
|
|
3743
|
+
};
|
|
3744
|
+
/** Whether a Worker's secret can sign previews: present and long enough. */
|
|
3745
|
+
export declare function canSignPreviews(secret: string | undefined): secret is string;
|
|
3746
|
+
/** A token for one entry of one site, valid for `PREVIEW_TTL_SECONDS` from `now`. */
|
|
3747
|
+
export declare function makePreviewToken(input: {
|
|
3748
|
+
readonly secret: string;
|
|
3749
|
+
readonly site: string;
|
|
3750
|
+
readonly entryId: string;
|
|
3751
|
+
readonly now: Date;
|
|
3752
|
+
}): Promise<{
|
|
3753
|
+
readonly token: string;
|
|
3754
|
+
readonly expiresAt: string;
|
|
3755
|
+
}>;
|
|
3756
|
+
/**
|
|
3757
|
+
* Checks a token for a site. The signature is verified (in constant time, by `crypto.subtle`)
|
|
3758
|
+
* before the payload is parsed, under the key of the site that is asked: a token made for another
|
|
3759
|
+
* site fails there. The entry the token names is the caller's to compare with the entry it read.
|
|
3760
|
+
*/
|
|
3761
|
+
export declare function verifyPreviewToken(input: {
|
|
3762
|
+
readonly secret: string | undefined;
|
|
3763
|
+
readonly site: string;
|
|
3764
|
+
readonly token: string;
|
|
3765
|
+
readonly now: Date;
|
|
3766
|
+
}): Promise<PreviewCheck>;
|
|
3767
|
+
//#endregion
|
|
3625
3768
|
//#region src/events.d.ts
|
|
3626
3769
|
/** A publish or an unpublish: the entry, and where it lives on the site. */
|
|
3627
3770
|
export declare const entryEvent: z.ZodObject<{
|
|
@@ -3762,4 +3905,4 @@ export declare function tagsOfSiteChange(collections: readonly string[], locales
|
|
|
3762
3905
|
//#region src/index.d.ts
|
|
3763
3906
|
export declare const name = "@hollware/core";
|
|
3764
3907
|
//#endregion
|
|
3765
|
-
export { type AcknowledgedWarning, type AuthDefinition, type BuiltInContractId, type BuiltInOptions, type CatalogRule, type Chunk, type CollectionDefinition, type CollectionDefinitionData, type ComponentDefinition, type ComponentUse, type ConfigProblem, type Contract, type ContractContext, type ContractDescription, type ContractFinding, type ContractText, type CustomContractOptions, type DescribedSchema, type DisallowedMarker, type Effect, type EntryDocument, type EntryEvent, type EntryFacts, type EntryLookup, type EntryRef, type ErrorCode, type ExtractedContent, type Heading, type HollwareConfig, type HollwareEvent, type IdPrefix, type ImageRef, type LinkRef, type LinkTarget, type Operation, type OperationByName, type OperationName, type OperationSpec, type OutcomeInfo, type OutcomeOf, type Phase, type Preset, type ProcessMarkdown, type ProcessMarkdownOptions, type ProcessResult, type ProcessedMarkdown, type RenderHtml, type ResolveConfig, type ResolvedAuth, type ResolvedCollection, type ResolvedConfig, type ResolvedSite, type RuleDefinition, type RunContractsInput, type SchemaOutput, type Scope, type Severity, type SiteDefinition, type SiteDefinitionData, type SiteDefinitionInput, type SiteDescription, type SiteEvent, type TaggedEntry, type Verdict, type Violation, schemas_d_exports as schemas };
|
|
3908
|
+
export { type AcknowledgedWarning, type AuthDefinition, type BuiltInContractId, type BuiltInOptions, type CatalogRule, type Chunk, type CollectionDefinition, type CollectionDefinitionData, type ComponentDefinition, type ComponentUse, type ConfigProblem, type Contract, type ContractContext, type ContractDescription, type ContractFinding, type ContractText, type CustomContractOptions, type DescribedSchema, type DisallowedMarker, type Effect, type EntryDocument, type EntryEvent, type EntryFacts, type EntryLookup, type EntryRef, type ErrorCode, type ExtractedContent, type Heading, type HollwareConfig, type HollwareEvent, type IdPrefix, type ImageRef, type LinkRef, type LinkTarget, type Operation, type OperationByName, type OperationName, type OperationSpec, type OutcomeInfo, type OutcomeOf, type Phase, type Preset, type PreviewCheck, type PreviewRefusal, type ProcessMarkdown, type ProcessMarkdownOptions, type ProcessResult, type ProcessedMarkdown, type RenderHtml, type ResolveConfig, type ResolvedAudit, type ResolvedAuth, type ResolvedCollection, type ResolvedConfig, type ResolvedSite, type RuleDefinition, type RunContractsInput, type SchemaOutput, type Scope, type Severity, type SiteDefinition, type SiteDefinitionData, type SiteDefinitionInput, type SiteDescription, type SiteEvent, type TaggedEntry, type Verdict, type Violation, schemas_d_exports as schemas };
|
package/dist/index.mjs
CHANGED
|
@@ -57,7 +57,7 @@ var Problems = class {
|
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/config/values.ts
|
|
59
59
|
/** A value with keys: not null, not an array. */
|
|
60
|
-
function isRecord$
|
|
60
|
+
function isRecord$4(value) {
|
|
61
61
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
62
|
}
|
|
63
63
|
/** A string as the messages show it: quoted, with what could break a line escaped. */
|
|
@@ -137,7 +137,7 @@ function readSiteUrl(path, raw, problems) {
|
|
|
137
137
|
/** The origins of a site's pages that may read its published content from the browser. */
|
|
138
138
|
function readCors(path, raw, problems) {
|
|
139
139
|
if (raw === void 0) return { origins: [] };
|
|
140
|
-
if (!isRecord$
|
|
140
|
+
if (!isRecord$4(raw) || !Array.isArray(raw["origins"])) {
|
|
141
141
|
problems.add(path, wrongType(raw, "an object with origins"), "Declare it as { origins: [\"https://example.com\"] }, or remove it.");
|
|
142
142
|
return { origins: [] };
|
|
143
143
|
}
|
|
@@ -158,7 +158,7 @@ function readBindingName(path, raw, problems) {
|
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
160
160
|
function readWebhook(path, raw, problems) {
|
|
161
|
-
if (!isRecord$
|
|
161
|
+
if (!isRecord$4(raw)) {
|
|
162
162
|
problems.add(path, wrongType(raw, "a webhook"), "Declare it as { name: \"newsletter\", url: \"https://…\", secret: \"WEBHOOK_SECRET\" }.");
|
|
163
163
|
return null;
|
|
164
164
|
}
|
|
@@ -183,7 +183,7 @@ function readEvents(path, raw, siteUrl, problems) {
|
|
|
183
183
|
indexNow: null
|
|
184
184
|
};
|
|
185
185
|
if (raw === void 0) return events;
|
|
186
|
-
if (!isRecord$
|
|
186
|
+
if (!isRecord$4(raw)) {
|
|
187
187
|
problems.add(path, wrongType(raw, "an object"), "Declare { webhooks: [ … ], indexNow: { … } }.");
|
|
188
188
|
return events;
|
|
189
189
|
}
|
|
@@ -207,7 +207,7 @@ function readEvents(path, raw, siteUrl, problems) {
|
|
|
207
207
|
const indexNow = raw["indexNow"];
|
|
208
208
|
if (indexNow !== void 0) {
|
|
209
209
|
const indexPath = keyPath(path, "indexNow");
|
|
210
|
-
if (!isRecord$
|
|
210
|
+
if (!isRecord$4(indexNow)) problems.add(indexPath, wrongType(indexNow, "an object"), "Declare { key: \"<your IndexNow key>\" }.");
|
|
211
211
|
else {
|
|
212
212
|
const raw = indexNow["key"];
|
|
213
213
|
const key = typeof raw === "string" && INDEXNOW_KEY.test(raw) ? raw : null;
|
|
@@ -219,6 +219,39 @@ function readEvents(path, raw, siteUrl, problems) {
|
|
|
219
219
|
return events;
|
|
220
220
|
}
|
|
221
221
|
//#endregion
|
|
222
|
+
//#region src/config/audit.ts
|
|
223
|
+
/** The fewest days a site may keep its activity log for, when it sets a retention (ADR-037). */
|
|
224
|
+
const AUDIT_RETENTION_MIN_DAYS = 30;
|
|
225
|
+
/** The most days a retention may be: ten years. Beyond that, keep it with no retention. */
|
|
226
|
+
const AUDIT_RETENTION_MAX_DAYS = 3650;
|
|
227
|
+
/** What a site that says nothing keeps: no addresses, and the log for as long as the site exists. */
|
|
228
|
+
const DEFAULT_AUDIT = {
|
|
229
|
+
ipAddresses: "off",
|
|
230
|
+
retentionDays: null
|
|
231
|
+
};
|
|
232
|
+
/** What the activity log of a site keeps, as its owner chose (ADR-037). */
|
|
233
|
+
function readAudit(path, raw, problems) {
|
|
234
|
+
if (raw === void 0 || raw === null) return DEFAULT_AUDIT;
|
|
235
|
+
if (!isRecord$4(raw)) {
|
|
236
|
+
problems.add(path, wrongType(raw, "an object"), "Declare it as { ipAddresses: \"off\", retentionDays: null }, or remove it.");
|
|
237
|
+
return DEFAULT_AUDIT;
|
|
238
|
+
}
|
|
239
|
+
let ipAddresses = "off";
|
|
240
|
+
const addresses = raw["ipAddresses"];
|
|
241
|
+
if (addresses === "hashed") ipAddresses = "hashed";
|
|
242
|
+
else if (addresses !== void 0 && addresses !== "off") problems.add(keyPath(path, "ipAddresses"), wrongType(addresses, "\"off\" or \"hashed\""), "Use \"off\" to keep no address, or \"hashed\" to keep a keyed hash of each.");
|
|
243
|
+
let retentionDays = null;
|
|
244
|
+
const days = raw["retentionDays"];
|
|
245
|
+
if (days !== void 0 && days !== null) {
|
|
246
|
+
if (typeof days !== "number" || !Number.isInteger(days) || days < 30 || days > 3650) problems.add(keyPath(path, "retentionDays"), `${typeof days === "number" ? String(days) : wrongType(days, "a number")} is not a whole number of days from 30 to ${AUDIT_RETENTION_MAX_DAYS}`, "Give the days to keep each line, or null to keep the log for as long as the site exists.");
|
|
247
|
+
else retentionDays = days;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
ipAddresses,
|
|
251
|
+
retentionDays
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
222
255
|
//#region src/config/constants.ts
|
|
223
256
|
/** The largest Markdown file a collection accepts when its config does not say, 512 KiB. */
|
|
224
257
|
const DEFAULT_MAX_MARKDOWN_BYTES = 524288;
|
|
@@ -289,7 +322,7 @@ function runtimeOf(contractId, context) {
|
|
|
289
322
|
*/
|
|
290
323
|
/** The most characters of the entry's own text that a message repeats. */
|
|
291
324
|
const MAX_ECHO_LENGTH = 80;
|
|
292
|
-
function isRecord$
|
|
325
|
+
function isRecord$3(value) {
|
|
293
326
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
294
327
|
}
|
|
295
328
|
/** Orders two texts by UTF-16 code units: the same in every runtime, unlike `localeCompare`. */
|
|
@@ -342,11 +375,11 @@ function fragmentAt(schema, segments) {
|
|
|
342
375
|
let node = schema;
|
|
343
376
|
for (const segment of segments) {
|
|
344
377
|
const properties = node.properties;
|
|
345
|
-
if (isRecord$
|
|
378
|
+
if (isRecord$3(properties) && Object.hasOwn(properties, segment)) {
|
|
346
379
|
const next = properties[segment];
|
|
347
|
-
if (!isRecord$
|
|
380
|
+
if (!isRecord$3(next)) return void 0;
|
|
348
381
|
node = next;
|
|
349
|
-
} else if (/^\d+$/.test(segment) && isRecord$
|
|
382
|
+
} else if (/^\d+$/.test(segment) && isRecord$3(node.items)) node = node.items;
|
|
350
383
|
else return;
|
|
351
384
|
}
|
|
352
385
|
return node;
|
|
@@ -354,7 +387,7 @@ function fragmentAt(schema, segments) {
|
|
|
354
387
|
function valueAt(input, segments) {
|
|
355
388
|
let node = input;
|
|
356
389
|
for (const segment of segments) if (Array.isArray(node) && /^\d+$/.test(segment)) node = node[Number(segment)];
|
|
357
|
-
else if (isRecord$
|
|
390
|
+
else if (isRecord$3(node) && Object.hasOwn(node, segment)) node = node[segment];
|
|
358
391
|
else return void 0;
|
|
359
392
|
return node;
|
|
360
393
|
}
|
|
@@ -390,11 +423,11 @@ function numberExpectation(type, schema) {
|
|
|
390
423
|
}
|
|
391
424
|
function arrayExpectation(schema) {
|
|
392
425
|
const size = range(schema.minItems, schema.maxItems, "item");
|
|
393
|
-
const items = isRecord$
|
|
426
|
+
const items = isRecord$3(schema.items) ? expectation(schema.items) : void 0;
|
|
394
427
|
return `an array${size === void 0 ? "" : ` of ${size}`}${items === void 0 ? "" : `, each ${items}`}`;
|
|
395
428
|
}
|
|
396
429
|
function objectExpectation(schema) {
|
|
397
|
-
const keys = isRecord$
|
|
430
|
+
const keys = isRecord$3(schema.properties) ? Object.keys(schema.properties) : [];
|
|
398
431
|
return keys.length === 0 ? "an object" : `an object with the keys ${keys.join(", ")}`;
|
|
399
432
|
}
|
|
400
433
|
function typeExpectation(type, schema) {
|
|
@@ -417,13 +450,13 @@ function expectation(schema) {
|
|
|
417
450
|
if ("const" in schema) return `exactly ${JSON.stringify(schema.const)}`;
|
|
418
451
|
if (Array.isArray(schema.enum)) return `one of ${listValues(schema.enum)}`;
|
|
419
452
|
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
420
|
-
const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$
|
|
453
|
+
const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$3(option) ? expectation(option) : void 0) : (Array.isArray(schema.type) ? schema.type : [schema.type]).map((type) => typeExpectation(type, schema));
|
|
421
454
|
return parts.every((part) => part !== void 0) ? parts.join(" or ") : void 0;
|
|
422
455
|
}
|
|
423
456
|
/** The keys of `value` that an object schema closed to other keys does not list. */
|
|
424
457
|
function unknownKeys(fragment, value) {
|
|
425
458
|
const { additionalProperties, properties } = fragment;
|
|
426
|
-
if (additionalProperties !== false || !isRecord$
|
|
459
|
+
if (additionalProperties !== false || !isRecord$3(properties) || !isRecord$3(value)) return [];
|
|
427
460
|
return Object.keys(value).filter((key) => !Object.hasOwn(properties, key));
|
|
428
461
|
}
|
|
429
462
|
/** The fix for one issue: imperative, naming the field and what the schema accepts there. */
|
|
@@ -433,7 +466,7 @@ function fixForIssue({ subject, schema, input, segments, message }) {
|
|
|
433
466
|
if (fragment !== void 0) {
|
|
434
467
|
const unknown = unknownKeys(fragment, valueAt(input, segments));
|
|
435
468
|
if (unknown.length > 0) {
|
|
436
|
-
const allowed = isRecord$
|
|
469
|
+
const allowed = isRecord$3(fragment.properties) ? Object.keys(fragment.properties) : [];
|
|
437
470
|
const nouns = unknown.length === 1 ? subject.noun : `${subject.noun}s`;
|
|
438
471
|
const where = allowed.length === 0 ? `no ${subject.noun}s are allowed there` : `the ${subject.noun}s allowed there are: ${allowed.join(", ")}`;
|
|
439
472
|
return `Remove the ${nouns} ${unknown.map((key) => quote(key)).join(", ")} from ${target}; ${where}.`;
|
|
@@ -786,7 +819,7 @@ function chunksOf(entry) {
|
|
|
786
819
|
//#endregion
|
|
787
820
|
//#region src/contracts/rules.ts
|
|
788
821
|
function present(frontmatter, field) {
|
|
789
|
-
return isRecord$
|
|
822
|
+
return isRecord$3(frontmatter) ? frontmatter[field] : void 0;
|
|
790
823
|
}
|
|
791
824
|
const GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
792
825
|
/** The length of a value as a reader counts it: characters of a text (an emoji is one), items of a list. */
|
|
@@ -891,7 +924,7 @@ function bannedWords(params, options = {}) {
|
|
|
891
924
|
check: (context) => {
|
|
892
925
|
if (pattern === null) return [];
|
|
893
926
|
const findings = [];
|
|
894
|
-
const frontmatter = isRecord$
|
|
927
|
+
const frontmatter = isRecord$3(context.frontmatter) ? context.frontmatter : {};
|
|
895
928
|
for (const [field, value] of Object.entries(frontmatter)) {
|
|
896
929
|
if (typeof value !== "string") continue;
|
|
897
930
|
for (const word of found(value)) findings.push({
|
|
@@ -932,7 +965,7 @@ function translationPair(options = {}) {
|
|
|
932
965
|
const field = collection.translationGroupField;
|
|
933
966
|
if (field === null) return [];
|
|
934
967
|
const path = `/frontmatter${pointer([field])}`;
|
|
935
|
-
const group = isRecord$
|
|
968
|
+
const group = isRecord$3(context.frontmatter) ? context.frontmatter[field] : void 0;
|
|
936
969
|
if (typeof group !== "string" || group === "") return [{
|
|
937
970
|
path,
|
|
938
971
|
message: `The entry has no translation group: its frontmatter field ${field} is empty.`,
|
|
@@ -1350,7 +1383,7 @@ const TARGET = "draft-2020-12";
|
|
|
1350
1383
|
*/
|
|
1351
1384
|
function toJsonSchema$1(schema) {
|
|
1352
1385
|
const converted = schema["~standard"].jsonSchema.input({ target: TARGET });
|
|
1353
|
-
if (!isRecord$
|
|
1386
|
+
if (!isRecord$4(converted)) throw new TypeError(`the library ${wrongType(converted, "a JSON Schema object")}`);
|
|
1354
1387
|
return JSON.parse(JSON.stringify(converted));
|
|
1355
1388
|
}
|
|
1356
1389
|
/** Reference chains longer than this are cycles. */
|
|
@@ -1362,7 +1395,7 @@ function pointTo(root, ref) {
|
|
|
1362
1395
|
let node = root;
|
|
1363
1396
|
for (const part of ref.slice(2).split("/")) {
|
|
1364
1397
|
const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
1365
|
-
if (!isRecord$
|
|
1398
|
+
if (!isRecord$4(node) || !Object.hasOwn(node, key)) return void 0;
|
|
1366
1399
|
node = node[key];
|
|
1367
1400
|
}
|
|
1368
1401
|
return node;
|
|
@@ -1374,7 +1407,7 @@ function pointTo(root, ref) {
|
|
|
1374
1407
|
function resolveSchema(root, schema) {
|
|
1375
1408
|
let node = schema;
|
|
1376
1409
|
for (let hop = 0; hop <= MAX_REF_HOPS; hop += 1) {
|
|
1377
|
-
if (!isRecord$
|
|
1410
|
+
if (!isRecord$4(node)) return null;
|
|
1378
1411
|
const ref = node["$ref"];
|
|
1379
1412
|
if (typeof ref !== "string") return node;
|
|
1380
1413
|
node = pointTo(root, ref);
|
|
@@ -1491,14 +1524,14 @@ function standardProps(value) {
|
|
|
1491
1524
|
/** A Standard Schema that also converts to JSON Schema, which is what a collection needs. */
|
|
1492
1525
|
function isDescribedSchema(value) {
|
|
1493
1526
|
const standard = standardProps(value);
|
|
1494
|
-
if (!isRecord$
|
|
1527
|
+
if (!isRecord$4(standard) || typeof standard["validate"] !== "function") return false;
|
|
1495
1528
|
const converter = standard["jsonSchema"];
|
|
1496
|
-
return isRecord$
|
|
1529
|
+
return isRecord$4(converter) && typeof converter["input"] === "function";
|
|
1497
1530
|
}
|
|
1498
1531
|
/** What `isDescribedSchema` found missing, for the message. */
|
|
1499
1532
|
function whyNotDescribed(value) {
|
|
1500
1533
|
const standard = standardProps(value);
|
|
1501
|
-
if (!isRecord$
|
|
1534
|
+
if (!isRecord$4(standard)) return wrongType(value, "a Standard Schema with JSON Schema");
|
|
1502
1535
|
if (typeof standard["validate"] !== "function") return "has no ~standard.validate function, so it is no Standard Schema";
|
|
1503
1536
|
return "has no ~standard.jsonSchema.input function, so describe_site could not show it to agents";
|
|
1504
1537
|
}
|
|
@@ -1568,7 +1601,7 @@ function claimRoute(route, path, site, problems) {
|
|
|
1568
1601
|
}
|
|
1569
1602
|
/** The route of each locale, in the order of the site's locales. */
|
|
1570
1603
|
function readRoutes(path, raw, collection, site, problems) {
|
|
1571
|
-
if (!isRecord$
|
|
1604
|
+
if (!isRecord$4(raw)) {
|
|
1572
1605
|
problems.add(path, wrongType(raw, "an object with one path per locale"), ROUTES_FIX);
|
|
1573
1606
|
return {};
|
|
1574
1607
|
}
|
|
@@ -1616,7 +1649,7 @@ function readField(path, raw, schema, kind, problems) {
|
|
|
1616
1649
|
return null;
|
|
1617
1650
|
}
|
|
1618
1651
|
if (schema === null) return raw;
|
|
1619
|
-
const properties = isRecord$
|
|
1652
|
+
const properties = isRecord$4(schema.object["properties"]) ? schema.object["properties"] : {};
|
|
1620
1653
|
if (!Object.hasOwn(properties, raw)) {
|
|
1621
1654
|
const names = Object.keys(properties);
|
|
1622
1655
|
problems.add(path, `${quote$1(raw)} is not a property of the schema${names.length === 0 ? ", which declares none" : ` (its properties are ${names.map(quote$1).join(", ")})`}`, `Name a field that the schema declares, or add ${quote$1(raw)} to the schema.`);
|
|
@@ -1625,7 +1658,7 @@ function readField(path, raw, schema, kind, problems) {
|
|
|
1625
1658
|
}
|
|
1626
1659
|
function readTranslations(path, raw, schema, problems) {
|
|
1627
1660
|
if (raw === void 0) return null;
|
|
1628
|
-
if (!isRecord$
|
|
1661
|
+
if (!isRecord$4(raw)) {
|
|
1629
1662
|
problems.add(path, wrongType(raw, "an object like { groupBy: \"translationGroup\" }"), "Set it to { groupBy: \"<field>\" }, the field of the frontmatter that holds the key the translations of an entry share, or remove it.");
|
|
1630
1663
|
return null;
|
|
1631
1664
|
}
|
|
@@ -1634,7 +1667,7 @@ function readTranslations(path, raw, schema, problems) {
|
|
|
1634
1667
|
const COMPONENT_FIX = "Declare it with defineComponent({ props: z.strictObject({ … }) }).";
|
|
1635
1668
|
function readComponent(path, name, raw, problems) {
|
|
1636
1669
|
if (!NAME.test(name)) problems.add(path, `${quote$1(name)} is not a valid component name`, nameFix("callout"));
|
|
1637
|
-
if (!isRecord$
|
|
1670
|
+
if (!isRecord$4(raw)) {
|
|
1638
1671
|
problems.add(path, wrongType(raw, "a component"), COMPONENT_FIX);
|
|
1639
1672
|
return null;
|
|
1640
1673
|
}
|
|
@@ -1652,7 +1685,7 @@ function readComponent(path, name, raw, problems) {
|
|
|
1652
1685
|
function readComponents(path, raw, problems) {
|
|
1653
1686
|
const components = /* @__PURE__ */ new Map();
|
|
1654
1687
|
if (raw === void 0) return components;
|
|
1655
|
-
if (!isRecord$
|
|
1688
|
+
if (!isRecord$4(raw)) {
|
|
1656
1689
|
problems.add(path, wrongType(raw, "an object that maps each component name to its definition"), COMPONENT_FIX);
|
|
1657
1690
|
return components;
|
|
1658
1691
|
}
|
|
@@ -1693,7 +1726,7 @@ function checkPhases(path, raw, problems) {
|
|
|
1693
1726
|
* what makes it a `Contract`. Whether it may have its id is a question for `checkIdentity`.
|
|
1694
1727
|
*/
|
|
1695
1728
|
function isContract(item, path, problems) {
|
|
1696
|
-
if (!isRecord$
|
|
1729
|
+
if (!isRecord$4(item)) {
|
|
1697
1730
|
problems.add(path, wrongType(item, "a contract"), "Write it as { id, severity, description, check }.");
|
|
1698
1731
|
return false;
|
|
1699
1732
|
}
|
|
@@ -1741,7 +1774,7 @@ function readContracts(path, raw, problems) {
|
|
|
1741
1774
|
if (isContract(item, itemAt, problems)) {
|
|
1742
1775
|
checkIdentity(item.id, isBuiltInContract(item), itemAt, ids, problems);
|
|
1743
1776
|
contracts.push(item);
|
|
1744
|
-
} else if (isRecord$
|
|
1777
|
+
} else if (isRecord$4(item) && typeof item["id"] === "string" && CONTRACT_ID.test(item["id"])) checkIdentity(item["id"], false, itemAt, ids, problems);
|
|
1745
1778
|
}
|
|
1746
1779
|
return contracts;
|
|
1747
1780
|
}
|
|
@@ -1757,7 +1790,7 @@ function readMaxMarkdownBytes(path, raw, problems) {
|
|
|
1757
1790
|
*/
|
|
1758
1791
|
function readCollection(path, name, raw, site, problems) {
|
|
1759
1792
|
if (!NAME.test(name)) problems.add(path, `${quote$1(name)} is not a valid collection name`, nameFix("blog"));
|
|
1760
|
-
if (!isRecord$
|
|
1793
|
+
if (!isRecord$4(raw)) {
|
|
1761
1794
|
problems.add(path, wrongType(raw, "a collection"), "Declare it with defineCollection({ schema, routes, … }).");
|
|
1762
1795
|
return null;
|
|
1763
1796
|
}
|
|
@@ -1861,7 +1894,7 @@ function readDefaultLocale(path, raw, locales, problems) {
|
|
|
1861
1894
|
const COLLECTIONS_FIX = "Declare at least one: collections: { blog: defineCollection({ schema, routes }) }.";
|
|
1862
1895
|
function readCollections(path, raw, site, problems) {
|
|
1863
1896
|
const collections = /* @__PURE__ */ new Map();
|
|
1864
|
-
if (!isRecord$
|
|
1897
|
+
if (!isRecord$4(raw)) {
|
|
1865
1898
|
problems.add(path, wrongType(raw, "an object that maps each collection name to its collection"), COLLECTIONS_FIX);
|
|
1866
1899
|
return collections;
|
|
1867
1900
|
}
|
|
@@ -1877,7 +1910,7 @@ function readCollections(path, raw, site, problems) {
|
|
|
1877
1910
|
function readSite(key, raw, problems) {
|
|
1878
1911
|
const path = keyPath("sites", key);
|
|
1879
1912
|
if (!isSiteKey(key)) problems.add(path, `${quote$1(key)} is not a valid site key`, SITE_KEY_FIX);
|
|
1880
|
-
if (!isRecord$
|
|
1913
|
+
if (!isRecord$4(raw)) {
|
|
1881
1914
|
problems.add(path, wrongType(raw, "a site"), "Declare it as { locales: [\"en\"], defaultLocale: \"en\", collections: { … } }.");
|
|
1882
1915
|
return null;
|
|
1883
1916
|
}
|
|
@@ -1899,12 +1932,13 @@ function readSite(key, raw, problems) {
|
|
|
1899
1932
|
collections,
|
|
1900
1933
|
url,
|
|
1901
1934
|
cors: readCors(keyPath(path, "cors"), raw["cors"], problems),
|
|
1902
|
-
events: readEvents(keyPath(path, "events"), raw["events"], url, problems)
|
|
1935
|
+
events: readEvents(keyPath(path, "events"), raw["events"], url, problems),
|
|
1936
|
+
audit: readAudit(keyPath(path, "audit"), raw["audit"], problems)
|
|
1903
1937
|
};
|
|
1904
1938
|
}
|
|
1905
1939
|
function readSites(raw, problems) {
|
|
1906
1940
|
const sites = /* @__PURE__ */ new Map();
|
|
1907
|
-
if (!isRecord$
|
|
1941
|
+
if (!isRecord$4(raw)) {
|
|
1908
1942
|
problems.add("sites", wrongType(raw, "an object that maps the site key to the site"), SITE_FIX);
|
|
1909
1943
|
return sites;
|
|
1910
1944
|
}
|
|
@@ -1919,7 +1953,7 @@ function readSites(raw, problems) {
|
|
|
1919
1953
|
/** `auth` of the config: absent, or an object whose `dynamicClientRegistration` is a boolean. */
|
|
1920
1954
|
function readAuth(raw, problems) {
|
|
1921
1955
|
if (raw === void 0) return { dynamicClientRegistration: false };
|
|
1922
|
-
if (!isRecord$
|
|
1956
|
+
if (!isRecord$4(raw)) {
|
|
1923
1957
|
problems.add("auth", wrongType(raw, "an object"), "Write auth: { dynamicClientRegistration: true }, or leave auth out.");
|
|
1924
1958
|
return { dynamicClientRegistration: false };
|
|
1925
1959
|
}
|
|
@@ -1961,7 +1995,7 @@ function resolveConfig(config) {
|
|
|
1961
1995
|
const problems = new Problems();
|
|
1962
1996
|
let sites = /* @__PURE__ */ new Map();
|
|
1963
1997
|
let auth = { dynamicClientRegistration: false };
|
|
1964
|
-
if (isRecord$
|
|
1998
|
+
if (isRecord$4(config)) {
|
|
1965
1999
|
sites = readSites(config["sites"], problems);
|
|
1966
2000
|
auth = readAuth(config["auth"], problems);
|
|
1967
2001
|
} else problems.add("config", wrongType(config, "an object"), "Export the result of defineConfig({ sites: { … } }) from hollware.config.ts.");
|
|
@@ -1972,6 +2006,69 @@ function resolveConfig(config) {
|
|
|
1972
2006
|
};
|
|
1973
2007
|
}
|
|
1974
2008
|
//#endregion
|
|
2009
|
+
//#region src/audit/index.ts
|
|
2010
|
+
/**
|
|
2011
|
+
* The actions of the activity log (ADR-037). A line names one of these, and nothing else: a new
|
|
2012
|
+
* operation or route that changes something is not logged until it is named here.
|
|
2013
|
+
*/
|
|
2014
|
+
/** Every operation that is not a read: each call of one is a line, whatever its outcome. */
|
|
2015
|
+
const OPERATION_AUDIT_ACTIONS = [
|
|
2016
|
+
"create_draft",
|
|
2017
|
+
"update_draft",
|
|
2018
|
+
"publish",
|
|
2019
|
+
"unpublish",
|
|
2020
|
+
"delete_entry",
|
|
2021
|
+
"restore_entry",
|
|
2022
|
+
"approve_review",
|
|
2023
|
+
"request_review",
|
|
2024
|
+
"comment_review",
|
|
2025
|
+
"request_changes",
|
|
2026
|
+
"update_site",
|
|
2027
|
+
"set_collection",
|
|
2028
|
+
"create_site",
|
|
2029
|
+
"delete_site",
|
|
2030
|
+
"restore_site",
|
|
2031
|
+
"make_webhook_secret"
|
|
2032
|
+
];
|
|
2033
|
+
/** What people do outside the operations: their tokens, members, passkeys, sessions and apps. */
|
|
2034
|
+
const IDENTITY_AUDIT_ACTIONS = [
|
|
2035
|
+
"token.create",
|
|
2036
|
+
"preview.create",
|
|
2037
|
+
"token.revoke",
|
|
2038
|
+
"member.role",
|
|
2039
|
+
"member.remove",
|
|
2040
|
+
"invitation.create",
|
|
2041
|
+
"invitation.accept",
|
|
2042
|
+
"session.sign_in",
|
|
2043
|
+
"session.sign_out",
|
|
2044
|
+
"session.end_all",
|
|
2045
|
+
"passkey.remove",
|
|
2046
|
+
"app.allow",
|
|
2047
|
+
"app.revoke",
|
|
2048
|
+
"app.refresh_reused",
|
|
2049
|
+
"schedule.publish"
|
|
2050
|
+
];
|
|
2051
|
+
const AUDIT_ACTIONS = [...OPERATION_AUDIT_ACTIONS, ...IDENTITY_AUDIT_ACTIONS];
|
|
2052
|
+
/** `denied`: a scope or a role was missing; `failed`: any other error the caller got. */
|
|
2053
|
+
const AUDIT_OUTCOMES = [
|
|
2054
|
+
"ok",
|
|
2055
|
+
"denied",
|
|
2056
|
+
"failed"
|
|
2057
|
+
];
|
|
2058
|
+
/**
|
|
2059
|
+
* Who acted: a person in the admin, a personal token, an app a person allowed through OAuth, or
|
|
2060
|
+
* the deployment itself (its cron).
|
|
2061
|
+
*/
|
|
2062
|
+
const AUDIT_ACTOR_KINDS = [
|
|
2063
|
+
"user",
|
|
2064
|
+
"token",
|
|
2065
|
+
"oauth_client",
|
|
2066
|
+
"system"
|
|
2067
|
+
];
|
|
2068
|
+
function isAuditAction(value) {
|
|
2069
|
+
return AUDIT_ACTIONS.includes(value);
|
|
2070
|
+
}
|
|
2071
|
+
//#endregion
|
|
1975
2072
|
//#region src/structure/definition.ts
|
|
1976
2073
|
/**
|
|
1977
2074
|
* The definition of a site as data (ADR-033): what D1 stores for it and what the operations that
|
|
@@ -2021,6 +2118,13 @@ const siteDefinitionData = z.strictObject({
|
|
|
2021
2118
|
webhooks: [],
|
|
2022
2119
|
indexNow: null
|
|
2023
2120
|
}),
|
|
2121
|
+
audit: z.strictObject({
|
|
2122
|
+
ipAddresses: z.enum(["off", "hashed"]).default("off").describe("Whether the activity log keeps the address of each request, as a hash keyed by the site, or nothing of it."),
|
|
2123
|
+
retentionDays: z.number().int().min(30).max(AUDIT_RETENTION_MAX_DAYS).nullable().default(null).describe("The days each line of the activity log is kept; null keeps it as long as the site.")
|
|
2124
|
+
}).default({
|
|
2125
|
+
ipAddresses: "off",
|
|
2126
|
+
retentionDays: null
|
|
2127
|
+
}).describe("What the activity log of the site keeps (ADR-037). Only an owner should change it."),
|
|
2024
2128
|
collections: z.record(z.string(), collectionDefinitionData)
|
|
2025
2129
|
});
|
|
2026
2130
|
//#endregion
|
|
@@ -2043,19 +2147,19 @@ const SUMMARIES = /* @__PURE__ */ new Set([
|
|
|
2043
2147
|
function segmentsOf$1(location) {
|
|
2044
2148
|
return location.replace(/^#\/?/, "").split("/").filter((segment) => segment !== "").map((segment) => decodeURIComponent(segment).replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
2045
2149
|
}
|
|
2046
|
-
function isRecord$
|
|
2150
|
+
function isRecord$2(value) {
|
|
2047
2151
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2048
2152
|
}
|
|
2049
2153
|
/** The schema of the object at `segments`, to tell a declared key from an unknown one. */
|
|
2050
2154
|
function declaredKeys(schema, segments) {
|
|
2051
2155
|
let node = schema;
|
|
2052
2156
|
for (const segment of segments) {
|
|
2053
|
-
if (!isRecord$
|
|
2157
|
+
if (!isRecord$2(node)) return /* @__PURE__ */ new Set();
|
|
2054
2158
|
const properties = node["properties"];
|
|
2055
|
-
node = isRecord$
|
|
2159
|
+
node = isRecord$2(properties) && Object.hasOwn(properties, segment) ? properties[segment] : node["items"];
|
|
2056
2160
|
}
|
|
2057
|
-
const properties = isRecord$
|
|
2058
|
-
return new Set(isRecord$
|
|
2161
|
+
const properties = isRecord$2(node) ? node["properties"] : void 0;
|
|
2162
|
+
return new Set(isRecord$2(properties) ? Object.keys(properties) : []);
|
|
2059
2163
|
}
|
|
2060
2164
|
/** The issues of a validation: one for each part that failed, at the path of the value. */
|
|
2061
2165
|
function issuesOf(schema, errors) {
|
|
@@ -2167,7 +2271,8 @@ function compileSite(path, site, problems) {
|
|
|
2167
2271
|
...webhook.secret === void 0 ? {} : { secret: webhook.secret }
|
|
2168
2272
|
})),
|
|
2169
2273
|
...site.events.indexNow === null ? {} : { indexNow: site.events.indexNow }
|
|
2170
|
-
}
|
|
2274
|
+
},
|
|
2275
|
+
audit: site.audit
|
|
2171
2276
|
};
|
|
2172
2277
|
}
|
|
2173
2278
|
/** The definitions of the sites, read from D1 as JSON, checked and resolved. */
|
|
@@ -2241,6 +2346,10 @@ function definitionsFromConfig(config) {
|
|
|
2241
2346
|
webhooks: (site.events?.webhooks ?? []).map((webhook) => ({ ...webhook })),
|
|
2242
2347
|
indexNow: site.events?.indexNow ?? null
|
|
2243
2348
|
},
|
|
2349
|
+
...site.audit === void 0 ? {} : { audit: {
|
|
2350
|
+
ipAddresses: site.audit.ipAddresses ?? "off",
|
|
2351
|
+
retentionDays: site.audit.retentionDays ?? null
|
|
2352
|
+
} },
|
|
2244
2353
|
collections: Object.fromEntries(Object.entries(site.collections).map(([name, collection]) => [name, collectionOf(keyPath(keyPath(path, "collections"), name), collection, problems)]))
|
|
2245
2354
|
};
|
|
2246
2355
|
}
|
|
@@ -2388,6 +2497,10 @@ function sitePreset(preset, locales, defaultLocale) {
|
|
|
2388
2497
|
webhooks: [],
|
|
2389
2498
|
indexNow: null
|
|
2390
2499
|
},
|
|
2500
|
+
audit: {
|
|
2501
|
+
ipAddresses: "off",
|
|
2502
|
+
retentionDays: null
|
|
2503
|
+
},
|
|
2391
2504
|
collections
|
|
2392
2505
|
};
|
|
2393
2506
|
}
|
|
@@ -2402,6 +2515,7 @@ const ERROR_CODES = [
|
|
|
2402
2515
|
"invalid_input",
|
|
2403
2516
|
"unknown_collection",
|
|
2404
2517
|
"unknown_locale",
|
|
2518
|
+
"invalid_preview",
|
|
2405
2519
|
"insufficient_scope",
|
|
2406
2520
|
"output_too_large",
|
|
2407
2521
|
"timeout",
|
|
@@ -2431,6 +2545,7 @@ var HollwareError = class extends Error {
|
|
|
2431
2545
|
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
2432
2546
|
/** The kinds of ids and their prefixes. A new kind adds its prefix here. */
|
|
2433
2547
|
const ID_PREFIXES = {
|
|
2548
|
+
auditLine: "aud",
|
|
2434
2549
|
challenge: "chl",
|
|
2435
2550
|
delivery: "dlv",
|
|
2436
2551
|
embedding: "emb",
|
|
@@ -3533,7 +3648,7 @@ function utf8ByteLength(text) {
|
|
|
3533
3648
|
}
|
|
3534
3649
|
return bytes;
|
|
3535
3650
|
}
|
|
3536
|
-
function isRecord(value) {
|
|
3651
|
+
function isRecord$1(value) {
|
|
3537
3652
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3538
3653
|
}
|
|
3539
3654
|
function describe(value) {
|
|
@@ -3566,8 +3681,8 @@ function linesBeforeBody(source, body) {
|
|
|
3566
3681
|
function yamlViolation(error, source) {
|
|
3567
3682
|
const reason = Reflect.get(error, "reason");
|
|
3568
3683
|
const mark = Reflect.get(error, "mark");
|
|
3569
|
-
const markLine = isRecord(mark) ? mark.line : void 0;
|
|
3570
|
-
const markColumn = isRecord(mark) ? mark.column : void 0;
|
|
3684
|
+
const markLine = isRecord$1(mark) ? mark.line : void 0;
|
|
3685
|
+
const markColumn = isRecord$1(mark) ? mark.column : void 0;
|
|
3571
3686
|
const firstLine = source.startsWith("---\r\n") ? 1 : 2;
|
|
3572
3687
|
return yamlSyntax(typeof reason === "string" ? reason : error.message.split("\n")[0] ?? "it does not parse", typeof markLine === "number" ? markLine + firstLine : void 0, typeof markColumn === "number" ? markColumn + 1 : void 0);
|
|
3573
3688
|
}
|
|
@@ -3593,7 +3708,7 @@ async function run(markdown, options) {
|
|
|
3593
3708
|
};
|
|
3594
3709
|
}
|
|
3595
3710
|
const frontmatter = split.data;
|
|
3596
|
-
if (!isRecord(frontmatter)) return {
|
|
3711
|
+
if (!isRecord$1(frontmatter)) return {
|
|
3597
3712
|
ok: false,
|
|
3598
3713
|
violations: [frontmatterNotMapping(describe(frontmatter))]
|
|
3599
3714
|
};
|
|
@@ -3700,7 +3815,20 @@ const URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
3700
3815
|
function isProps(value) {
|
|
3701
3816
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3702
3817
|
}
|
|
3703
|
-
|
|
3818
|
+
/** The value a path such as `frontmatter.url` names in the data, or `undefined`. */
|
|
3819
|
+
function resolvePath(data, path) {
|
|
3820
|
+
let value = data;
|
|
3821
|
+
for (const part of path.split(".")) {
|
|
3822
|
+
if (typeof value !== "object" || value === null || !Object.hasOwn(value, part)) return void 0;
|
|
3823
|
+
value = Reflect.get(value, part);
|
|
3824
|
+
}
|
|
3825
|
+
return value;
|
|
3826
|
+
}
|
|
3827
|
+
/** The name of an attribute without the `:` or `v-bind:` of a binding. */
|
|
3828
|
+
function unbound(key) {
|
|
3829
|
+
return key.startsWith("v-bind:") ? key.slice(7) : key.replace(/^:/, "");
|
|
3830
|
+
}
|
|
3831
|
+
function sanitizeProps(raw, data) {
|
|
3704
3832
|
const props = {};
|
|
3705
3833
|
if (!isProps(raw)) return props;
|
|
3706
3834
|
for (const key of Object.keys(raw)) {
|
|
@@ -3709,8 +3837,15 @@ function sanitizeProps(raw) {
|
|
|
3709
3837
|
if (isForbiddenAttribute(name)) continue;
|
|
3710
3838
|
const value = raw[key];
|
|
3711
3839
|
if (URL_ATTRIBUTES.has(name)) {
|
|
3840
|
+
const kind = name === "src" ? "image" : "link";
|
|
3712
3841
|
const url = urlValue(key, value);
|
|
3713
|
-
if (url === void 0
|
|
3842
|
+
if (url === void 0) continue;
|
|
3843
|
+
if (isBinding(key) && url === value) {
|
|
3844
|
+
const bound = resolvePath(data, url);
|
|
3845
|
+
if (typeof bound === "string" && isSafeUrl(bound, kind)) setOwn(props, unbound(key), bound.replaceAll("&", "&"));
|
|
3846
|
+
continue;
|
|
3847
|
+
}
|
|
3848
|
+
if (!isSafeUrl(url, kind)) continue;
|
|
3714
3849
|
}
|
|
3715
3850
|
setOwn(props, key, typeof value === "string" && !isBinding(key) ? value.replaceAll("&", "&") : value);
|
|
3716
3851
|
}
|
|
@@ -3722,19 +3857,19 @@ function marker(name) {
|
|
|
3722
3857
|
name: TAG_NAME.test(name) ? shorten(name) : "?"
|
|
3723
3858
|
}];
|
|
3724
3859
|
}
|
|
3725
|
-
function sanitizeElement(node, depth) {
|
|
3860
|
+
function sanitizeElement(node, depth, data) {
|
|
3726
3861
|
const tag = node[0];
|
|
3727
3862
|
if (typeof tag !== "string") return void 0;
|
|
3728
3863
|
if (!NATIVE_TAGS.has(tag) && tag !== "hollware-marker") {
|
|
3729
3864
|
if (!TAG_NAME.test(tag) || DANGEROUS_TAGS.has(tag.toLowerCase())) return marker(tag);
|
|
3730
3865
|
}
|
|
3731
|
-
const element = [tag, sanitizeProps(node[1])];
|
|
3866
|
+
const element = [tag, sanitizeProps(node[1], data)];
|
|
3732
3867
|
if (depth >= 100) return element;
|
|
3733
3868
|
for (let at = 2; at < node.length; at++) {
|
|
3734
3869
|
const child = node[at];
|
|
3735
3870
|
if (typeof child === "string") element.push(child);
|
|
3736
3871
|
else if (Array.isArray(child)) {
|
|
3737
|
-
const clean = sanitizeElement(child, depth + 1);
|
|
3872
|
+
const clean = sanitizeElement(child, depth + 1, data);
|
|
3738
3873
|
if (clean !== void 0) element.push(clean);
|
|
3739
3874
|
}
|
|
3740
3875
|
}
|
|
@@ -3743,10 +3878,14 @@ function sanitizeElement(node, depth) {
|
|
|
3743
3878
|
/** Copies a document through the rules of this file. */
|
|
3744
3879
|
function sanitizeDocument(document) {
|
|
3745
3880
|
const nodes = [];
|
|
3881
|
+
const data = {
|
|
3882
|
+
frontmatter: document.frontmatter,
|
|
3883
|
+
meta: document.meta
|
|
3884
|
+
};
|
|
3746
3885
|
if (Array.isArray(document.nodes)) {
|
|
3747
3886
|
for (const node of document.nodes) if (typeof node === "string") nodes.push(node);
|
|
3748
3887
|
else if (Array.isArray(node)) {
|
|
3749
|
-
const clean = sanitizeElement(node, 1);
|
|
3888
|
+
const clean = sanitizeElement(node, 1, data);
|
|
3750
3889
|
if (clean !== void 0) nodes.push(clean);
|
|
3751
3890
|
}
|
|
3752
3891
|
}
|
|
@@ -4681,13 +4820,13 @@ const TIMESTAMP_HEADER = "hollware-timestamp";
|
|
|
4681
4820
|
const EVENT_ID_HEADER = "hollware-event-id";
|
|
4682
4821
|
/** How far a timestamp may be from the receiver's clock, by default: five minutes. */
|
|
4683
4822
|
const TOLERANCE_SECONDS = 300;
|
|
4684
|
-
const encoder = new TextEncoder();
|
|
4823
|
+
const encoder$1 = new TextEncoder();
|
|
4685
4824
|
async function hmac(secret, text) {
|
|
4686
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
4825
|
+
const key = await crypto.subtle.importKey("raw", encoder$1.encode(secret), {
|
|
4687
4826
|
name: "HMAC",
|
|
4688
4827
|
hash: "SHA-256"
|
|
4689
4828
|
}, false, ["sign"]);
|
|
4690
|
-
return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(text)))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4829
|
+
return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder$1.encode(text)))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4691
4830
|
}
|
|
4692
4831
|
/** The value of `hollware-signature` for a body sent at `timestamp` (seconds). */
|
|
4693
4832
|
async function signWebhook(secret, timestamp, body) {
|
|
@@ -4700,9 +4839,20 @@ function sameText(a, b) {
|
|
|
4700
4839
|
for (let index = 0; index < a.length; index++) difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
4701
4840
|
return difference === 0;
|
|
4702
4841
|
}
|
|
4842
|
+
/** The id of the event a body holds, or `null` when it holds none. */
|
|
4843
|
+
function eventIdOf(body) {
|
|
4844
|
+
try {
|
|
4845
|
+
const value = JSON.parse(body);
|
|
4846
|
+
const id = typeof value === "object" && value !== null ? Reflect.get(value, "id") : null;
|
|
4847
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
4848
|
+
} catch {
|
|
4849
|
+
return null;
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4703
4852
|
/**
|
|
4704
4853
|
* Whether a request is a webhook of Hollware: its signature matches the body and the secret, and
|
|
4705
|
-
* its timestamp is within the tolerance, so a captured request cannot be replayed later
|
|
4854
|
+
* its timestamp is within the tolerance, so a captured request cannot be replayed later; with
|
|
4855
|
+
* `isNew`, also that its event was not received before, so it cannot be replayed sooner either.
|
|
4706
4856
|
*/
|
|
4707
4857
|
async function verifyWebhook(check) {
|
|
4708
4858
|
const signature = check.headers.get(SIGNATURE_HEADER);
|
|
@@ -4710,7 +4860,139 @@ async function verifyWebhook(check) {
|
|
|
4710
4860
|
if (signature === null || !Number.isInteger(timestamp)) return false;
|
|
4711
4861
|
const now = check.now ?? Math.floor(Date.now() / 1e3);
|
|
4712
4862
|
if (Math.abs(now - timestamp) > (check.toleranceSeconds ?? TOLERANCE_SECONDS)) return false;
|
|
4713
|
-
|
|
4863
|
+
if (!sameText(signature, await signWebhook(check.secret, timestamp, check.body))) return false;
|
|
4864
|
+
if (check.isNew === void 0) return true;
|
|
4865
|
+
const id = eventIdOf(check.body);
|
|
4866
|
+
return id !== null && await check.isNew(id);
|
|
4867
|
+
}
|
|
4868
|
+
//#endregion
|
|
4869
|
+
//#region src/preview.ts
|
|
4870
|
+
/**
|
|
4871
|
+
* The preview token (T-103 of docs/security/threat-model.md): a signed, stateless proof that a person of a site may read the
|
|
4872
|
+
* latest revision of one entry, for one hour. A site that reads published content only (the
|
|
4873
|
+
* binding of RPC, the read API) reads that entry's head instead when it presents one.
|
|
4874
|
+
*
|
|
4875
|
+
* hw_prv_<payload>.<signature>
|
|
4876
|
+
*
|
|
4877
|
+
* The payload is base64url of `{ "s": site, "e": entryId, "x": expiresAtSeconds }`, and the
|
|
4878
|
+
* signature the base64url HMAC-SHA256 of the payload text under a key that HKDF-SHA256 derives
|
|
4879
|
+
* from the Worker secret `HOLLWARE_SECRET`, bound to the site. Nothing is stored: a token cannot
|
|
4880
|
+
* be revoked, only wait out its hour, and rotating the secret ends every one.
|
|
4881
|
+
*/
|
|
4882
|
+
const PREVIEW_TOKEN_PREFIX = "hw_prv_";
|
|
4883
|
+
/** The query parameter a site's page takes the token from. */
|
|
4884
|
+
const PREVIEW_QUERY_PARAM = "hollware-preview";
|
|
4885
|
+
/** How long a token lasts, in seconds. */
|
|
4886
|
+
const PREVIEW_TTL_SECONDS = 3600;
|
|
4887
|
+
/** The least characters of `HOLLWARE_SECRET` that Hollware signs anything with. */
|
|
4888
|
+
const PREVIEW_SECRET_MIN_LENGTH = 32;
|
|
4889
|
+
/** Whether a Worker's secret can sign previews: present and long enough. */
|
|
4890
|
+
function canSignPreviews(secret) {
|
|
4891
|
+
return secret !== void 0 && secret.length >= 32;
|
|
4892
|
+
}
|
|
4893
|
+
const encoder = new TextEncoder();
|
|
4894
|
+
function toBase64Url(bytes) {
|
|
4895
|
+
let binary = "";
|
|
4896
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4897
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
4898
|
+
}
|
|
4899
|
+
function fromBase64Url(text) {
|
|
4900
|
+
if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;
|
|
4901
|
+
try {
|
|
4902
|
+
const binary = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
|
|
4903
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
4904
|
+
} catch {
|
|
4905
|
+
return null;
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
async function previewKey(secret, site) {
|
|
4909
|
+
const base = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, ["deriveKey"]);
|
|
4910
|
+
return crypto.subtle.deriveKey({
|
|
4911
|
+
name: "HKDF",
|
|
4912
|
+
hash: "SHA-256",
|
|
4913
|
+
salt: /* @__PURE__ */ new Uint8Array(0),
|
|
4914
|
+
info: encoder.encode(`hollware preview\n${site}`)
|
|
4915
|
+
}, base, {
|
|
4916
|
+
name: "HMAC",
|
|
4917
|
+
hash: "SHA-256",
|
|
4918
|
+
length: 256
|
|
4919
|
+
}, false, ["sign", "verify"]);
|
|
4920
|
+
}
|
|
4921
|
+
/** A token for one entry of one site, valid for `PREVIEW_TTL_SECONDS` from `now`. */
|
|
4922
|
+
async function makePreviewToken(input) {
|
|
4923
|
+
if (!canSignPreviews(input.secret)) throw new Error(`HOLLWARE_SECRET must be at least 32 characters.`);
|
|
4924
|
+
const expires = Math.floor(input.now.getTime() / 1e3) + PREVIEW_TTL_SECONDS;
|
|
4925
|
+
const payload = toBase64Url(encoder.encode(JSON.stringify({
|
|
4926
|
+
s: input.site,
|
|
4927
|
+
e: input.entryId,
|
|
4928
|
+
x: expires
|
|
4929
|
+
})));
|
|
4930
|
+
const mac = await crypto.subtle.sign("HMAC", await previewKey(input.secret, input.site), encoder.encode(payload));
|
|
4931
|
+
return {
|
|
4932
|
+
token: `${PREVIEW_TOKEN_PREFIX}${payload}.${toBase64Url(new Uint8Array(mac))}`,
|
|
4933
|
+
expiresAt: (/* @__PURE__ */ new Date(expires * 1e3)).toISOString()
|
|
4934
|
+
};
|
|
4935
|
+
}
|
|
4936
|
+
function isRecord(value) {
|
|
4937
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* Checks a token for a site. The signature is verified (in constant time, by `crypto.subtle`)
|
|
4941
|
+
* before the payload is parsed, under the key of the site that is asked: a token made for another
|
|
4942
|
+
* site fails there. The entry the token names is the caller's to compare with the entry it read.
|
|
4943
|
+
*/
|
|
4944
|
+
async function verifyPreviewToken(input) {
|
|
4945
|
+
if (!canSignPreviews(input.secret)) return {
|
|
4946
|
+
ok: false,
|
|
4947
|
+
reason: "no_secret"
|
|
4948
|
+
};
|
|
4949
|
+
const { token } = input;
|
|
4950
|
+
if (!token.startsWith("hw_prv_")) return {
|
|
4951
|
+
ok: false,
|
|
4952
|
+
reason: "malformed"
|
|
4953
|
+
};
|
|
4954
|
+
const parts = token.slice(7).split(".");
|
|
4955
|
+
const [payload, signature] = parts;
|
|
4956
|
+
if (parts.length !== 2 || payload === void 0 || signature === void 0) return {
|
|
4957
|
+
ok: false,
|
|
4958
|
+
reason: "malformed"
|
|
4959
|
+
};
|
|
4960
|
+
const mac = fromBase64Url(signature);
|
|
4961
|
+
const body = fromBase64Url(payload);
|
|
4962
|
+
if (mac === null || body === null) return {
|
|
4963
|
+
ok: false,
|
|
4964
|
+
reason: "malformed"
|
|
4965
|
+
};
|
|
4966
|
+
if (!await crypto.subtle.verify("HMAC", await previewKey(input.secret, input.site), mac, encoder.encode(payload))) return {
|
|
4967
|
+
ok: false,
|
|
4968
|
+
reason: "signature"
|
|
4969
|
+
};
|
|
4970
|
+
let claims;
|
|
4971
|
+
try {
|
|
4972
|
+
claims = JSON.parse(new TextDecoder().decode(body));
|
|
4973
|
+
} catch {
|
|
4974
|
+
return {
|
|
4975
|
+
ok: false,
|
|
4976
|
+
reason: "malformed"
|
|
4977
|
+
};
|
|
4978
|
+
}
|
|
4979
|
+
if (!isRecord(claims) || typeof claims["s"] !== "string" || typeof claims["e"] !== "string" || typeof claims["x"] !== "number") return {
|
|
4980
|
+
ok: false,
|
|
4981
|
+
reason: "malformed"
|
|
4982
|
+
};
|
|
4983
|
+
if (claims["s"] !== input.site) return {
|
|
4984
|
+
ok: false,
|
|
4985
|
+
reason: "site"
|
|
4986
|
+
};
|
|
4987
|
+
if (Math.floor(input.now.getTime() / 1e3) >= claims["x"]) return {
|
|
4988
|
+
ok: false,
|
|
4989
|
+
reason: "expired"
|
|
4990
|
+
};
|
|
4991
|
+
return {
|
|
4992
|
+
ok: true,
|
|
4993
|
+
entryId: claims["e"],
|
|
4994
|
+
expiresAt: (/* @__PURE__ */ new Date(claims["x"] * 1e3)).toISOString()
|
|
4995
|
+
};
|
|
4714
4996
|
}
|
|
4715
4997
|
//#endregion
|
|
4716
4998
|
//#region src/events.ts
|
|
@@ -4835,4 +5117,4 @@ function tagsOfSiteChange(collections, locales) {
|
|
|
4835
5117
|
//#region src/index.ts
|
|
4836
5118
|
const name = "@hollware/core";
|
|
4837
5119
|
//#endregion
|
|
4838
|
-
export { BUILT_IN_CONTRACT_IDS, CATALOG, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, EVERY_LOCALE, HollwareError, ID_PREFIXES, INDEXNOW_KEY, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, PRESETS, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, SITE_TAG, TIMESTAMP_HEADER, approveReview, canonicalJson, catalogRule, chunksOf, collectionDefinitionData, collectionPreset, commentReview, compileSites, componentDefinitionData, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, definitionsFromConfig, deleteEntry, describeBuiltInContracts, describeCatalog, describeContract, describeResolvedSite, describeSite, describedSchemaOf, entryEvent, entryTag, generatePersonalToken, generateReadToken, getEntry, getTranslations, groupTag, hashPersonalToken, hollwareEvent, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, listTag, name, newId, parseHollwareEvent, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, restoreEntry, routesFor, ruleDefinition, runContracts, schemas_exports as schemas, search, signWebhook, siteDefinitionData, siteEvent, sitePreset, sizeLimitViolation, tagsOfChange, tagsOfEntries, tagsOfSiteChange, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook, webhookDefinition };
|
|
5120
|
+
export { AUDIT_ACTIONS, AUDIT_ACTOR_KINDS, AUDIT_OUTCOMES, AUDIT_RETENTION_MAX_DAYS, AUDIT_RETENTION_MIN_DAYS, BUILT_IN_CONTRACT_IDS, CATALOG, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_AUDIT, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, EVERY_LOCALE, HollwareError, IDENTITY_AUDIT_ACTIONS, ID_PREFIXES, INDEXNOW_KEY, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, OPERATION_AUDIT_ACTIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, PRESETS, PREVIEW_QUERY_PARAM, PREVIEW_SECRET_MIN_LENGTH, PREVIEW_TOKEN_PREFIX, PREVIEW_TTL_SECONDS, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, SITE_TAG, TIMESTAMP_HEADER, approveReview, canSignPreviews, canonicalJson, catalogRule, chunksOf, collectionDefinitionData, collectionPreset, commentReview, compileSites, componentDefinitionData, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, definitionsFromConfig, deleteEntry, describeBuiltInContracts, describeCatalog, describeContract, describeResolvedSite, describeSite, describedSchemaOf, entryEvent, entryTag, generatePersonalToken, generateReadToken, getEntry, getTranslations, groupTag, hashPersonalToken, hollwareEvent, isAuditAction, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, listTag, makePreviewToken, name, newId, parseHollwareEvent, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, restoreEntry, routesFor, ruleDefinition, runContracts, schemas_exports as schemas, search, signWebhook, siteDefinitionData, siteEvent, sitePreset, sizeLimitViolation, tagsOfChange, tagsOfEntries, tagsOfSiteChange, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyPreviewToken, verifyWebhook, webhookDefinition };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hollware/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
4
|
"description": "Contracts, operation specs, schemas and the Markdown pipeline of Hollware. No Cloudflare dependencies.",
|
|
5
5
|
"homepage": "https://hollware.com",
|
|
6
6
|
"bugs": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
21
22
|
"types": "./dist/index.d.mts",
|