@cloudhal/storage 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -2
  3. package/dist/migrations/0001_init.d.ts +12 -0
  4. package/dist/migrations/0001_init.js +46 -0
  5. package/dist/migrations/0002_auth.d.ts +7 -0
  6. package/dist/migrations/0002_auth.js +26 -0
  7. package/dist/migrations/0003_agent.d.ts +6 -0
  8. package/dist/migrations/0003_agent.js +32 -0
  9. package/dist/migrations/0004_audit_actor_name.d.ts +3 -0
  10. package/dist/migrations/0004_audit_actor_name.js +7 -0
  11. package/dist/migrations/0005_auth_rate_limit.d.ts +7 -0
  12. package/dist/migrations/0005_auth_rate_limit.js +11 -0
  13. package/dist/migrations/0006_approvals.d.ts +7 -0
  14. package/dist/migrations/0006_approvals.js +31 -0
  15. package/dist/migrations/0007_audit_mode.d.ts +3 -0
  16. package/dist/migrations/0007_audit_mode.js +7 -0
  17. package/dist/migrations/0008_policy_detail.d.ts +7 -0
  18. package/dist/migrations/0008_policy_detail.js +17 -0
  19. package/dist/migrations/0009_audit_elevation.d.ts +3 -0
  20. package/dist/migrations/0009_audit_elevation.js +7 -0
  21. package/dist/migrations/0010_approval_result.d.ts +6 -0
  22. package/dist/migrations/0010_approval_result.js +10 -0
  23. package/dist/migrations/0011_op_guards.d.ts +17 -0
  24. package/dist/migrations/0011_op_guards.js +30 -0
  25. package/dist/migrations/0012_roles.d.ts +23 -0
  26. package/dist/migrations/0012_roles.js +78 -0
  27. package/dist/migrations/0013_media.d.ts +16 -0
  28. package/dist/migrations/0013_media.js +69 -0
  29. package/dist/migrations/0014_analytics.d.ts +19 -0
  30. package/dist/migrations/0014_analytics.js +105 -0
  31. package/dist/migrations/0015_analytics_privacy_signal.d.ts +8 -0
  32. package/dist/migrations/0015_analytics_privacy_signal.js +14 -0
  33. package/dist/migrations/0016_comments.d.ts +27 -0
  34. package/dist/migrations/0016_comments.js +173 -0
  35. package/dist/migrations/0017_schedules.d.ts +21 -0
  36. package/dist/migrations/0017_schedules.js +108 -0
  37. package/dist/migrations/0018_query_indexes.d.ts +12 -0
  38. package/dist/migrations/0018_query_indexes.js +22 -0
  39. package/dist/migrations/0019_body_proposals.d.ts +8 -0
  40. package/dist/migrations/0019_body_proposals.js +24 -0
  41. package/dist/migrations/0020_ai.d.ts +8 -0
  42. package/dist/migrations/0020_ai.js +31 -0
  43. package/dist/migrations/0021_search.d.ts +12 -0
  44. package/dist/migrations/0021_search.js +29 -0
  45. package/dist/migrations/0022_search_kinds.d.ts +12 -0
  46. package/dist/migrations/0022_search_kinds.js +35 -0
  47. package/dist/migrations/0023_search_usage.d.ts +7 -0
  48. package/dist/migrations/0023_search_usage.js +18 -0
  49. package/dist/migrations/0024_search_quota.d.ts +8 -0
  50. package/dist/migrations/0024_search_quota.js +18 -0
  51. package/dist/migrations/0025_translation_groups.d.ts +7 -0
  52. package/dist/migrations/0025_translation_groups.js +15 -0
  53. package/dist/migrations/0026_search_cjk.d.ts +8 -0
  54. package/dist/migrations/0026_search_cjk.js +20 -0
  55. package/dist/migrations/0027_multi_site.d.ts +14 -0
  56. package/dist/migrations/0027_multi_site.js +44 -0
  57. package/dist/migrations/0028_site_members.d.ts +8 -0
  58. package/dist/migrations/0028_site_members.js +22 -0
  59. package/dist/migrations/index.d.ts +3 -0
  60. package/dist/migrations/index.js +61 -0
  61. package/dist/src/blob/fs.d.ts +19 -0
  62. package/dist/src/blob/fs.js +172 -0
  63. package/dist/src/blob/memory.d.ts +18 -0
  64. package/dist/src/blob/memory.js +79 -0
  65. package/dist/src/blob/r2.d.ts +19 -0
  66. package/dist/src/blob/r2.js +146 -0
  67. package/dist/src/index.d.ts +11 -0
  68. package/dist/src/index.js +12 -0
  69. package/dist/src/migrate.d.ts +17 -0
  70. package/dist/src/migrate.js +67 -0
  71. package/dist/src/sql/better-sqlite3.d.ts +23 -0
  72. package/dist/src/sql/better-sqlite3.js +104 -0
  73. package/dist/src/sql/d1.d.ts +13 -0
  74. package/dist/src/sql/d1.js +70 -0
  75. package/dist/src/types.d.ts +131 -0
  76. package/dist/src/types.js +235 -0
  77. package/package.json +76 -3
@@ -0,0 +1,78 @@
1
+
2
+
3
+ /**
4
+ * Staff roles and invites (RFC-0005 §11, ADR-0018). Additive.
5
+ *
6
+ * 1. `invitation` columns, declared to Better Auth as the organization plugin's invitation
7
+ * `additionalFields` (`packages/api/src/auth.ts`); the types are what Better Auth 1.7.7 emits for
8
+ * them (`string` → text, `date` → date). The token hash is unique (only `SHA-256(token)` is
9
+ * stored; RFC-0005 §7.1).
10
+ * 2. The deployment organization (`id` and `slug` `deployment`, named after the first site), unless
11
+ * it exists.
12
+ * 3. A `member` row for every existing user: the earliest (`createdAt`, then `id`) becomes `owner`,
13
+ * any others `administrator` (the closest role to their former `*` that keeps exactly one Owner).
14
+ * Two unique indexes then hold the invariants in the database itself: one membership per user
15
+ * and organization, and at most one Owner (I2).
16
+ * 4. `agent_owner`: which person an agent actor belongs to (the creator of the key it used), kept
17
+ * after the key is deleted so ownership of agent-written entries survives (RFC-0005 §4; the
18
+ * server-side form of the optional `Actor.onBehalfOf`, §8). Back-filled from `apikey`.
19
+ * 5. `password_reset`: administrator-issued one-time reset links (open question 6), stored like
20
+ * invite tokens (hash only).
21
+ *
22
+ * Keys are not changed: their effective scopes are computed per call (§5.1).
23
+ */
24
+ export const migration0012Roles = {
25
+ name: "0012_roles",
26
+ sql: `
27
+ ALTER TABLE "invitation" ADD COLUMN "tokenHash" text;
28
+ ALTER TABLE "invitation" ADD COLUMN "delivery" text;
29
+ ALTER TABLE "invitation" ADD COLUMN "acceptedUserId" text;
30
+ ALTER TABLE "invitation" ADD COLUMN "acceptedAt" date;
31
+ ALTER TABLE "invitation" ADD COLUMN "canceledBy" text;
32
+ ALTER TABLE "invitation" ADD COLUMN "canceledAt" date;
33
+ CREATE UNIQUE INDEX "invitation_tokenHash_uidx" ON "invitation" ("tokenHash");
34
+ CREATE INDEX "invitation_status_idx" ON "invitation" ("organizationId", "status");
35
+ INSERT INTO "organization" ("id", "name", "slug", "createdAt")
36
+ SELECT 'deployment',
37
+ COALESCE((SELECT json_extract(body, '$.name') FROM nodes WHERE kind = 'site' ORDER BY created_at, id LIMIT 1), 'Cloudhal'),
38
+ 'deployment',
39
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
40
+ WHERE NOT EXISTS (SELECT 1 FROM "organization" WHERE "id" = 'deployment');
41
+ INSERT INTO "member" ("id", "organizationId", "userId", "role", "createdAt")
42
+ SELECT 'mem_' || u."id",
43
+ 'deployment',
44
+ u."id",
45
+ CASE
46
+ WHEN u."id" = (SELECT "id" FROM "user" ORDER BY "createdAt", "id" LIMIT 1)
47
+ AND NOT EXISTS (SELECT 1 FROM "member" WHERE "organizationId" = 'deployment' AND "role" = 'owner')
48
+ THEN 'owner'
49
+ ELSE 'administrator'
50
+ END,
51
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
52
+ FROM "user" u
53
+ WHERE NOT EXISTS (SELECT 1 FROM "member" m WHERE m."userId" = u."id" AND m."organizationId" = 'deployment');
54
+ CREATE UNIQUE INDEX "member_org_user_uidx" ON "member" ("organizationId", "userId");
55
+ CREATE UNIQUE INDEX "member_one_owner_uidx" ON "member" ("organizationId") WHERE "role" = 'owner';
56
+ CREATE TABLE agent_owner (
57
+ agent_id TEXT PRIMARY KEY,
58
+ user_id TEXT NOT NULL,
59
+ key_id TEXT,
60
+ created_at TEXT NOT NULL
61
+ );
62
+ CREATE INDEX agent_owner_user ON agent_owner (user_id);
63
+ INSERT INTO agent_owner (agent_id, user_id, key_id, created_at)
64
+ SELECT 'agent_' || substr("id", 1, 32), "referenceId", "id", strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
65
+ FROM "apikey"
66
+ WHERE length("id") >= 6 AND "id" NOT GLOB '*[^A-Za-z0-9]*';
67
+ CREATE TABLE password_reset (
68
+ id TEXT PRIMARY KEY,
69
+ token_hash TEXT NOT NULL UNIQUE,
70
+ user_id TEXT NOT NULL,
71
+ created_by TEXT NOT NULL,
72
+ created_at TEXT NOT NULL,
73
+ expires_at TEXT NOT NULL,
74
+ used_at TEXT
75
+ );
76
+ CREATE INDEX password_reset_user ON password_reset (user_id);
77
+ `,
78
+ };
@@ -0,0 +1,16 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * Assets and the media library (RFC-0004 §4.2). Asset nodes need no change of their own (`nodes.kind`
4
+ * is free text; their uniqueness key is the SHA-256). These tables hold what is not a node:
5
+ *
6
+ * - media_blobs: the ledger of content-addressed bytes per site (`media/<siteId>/<sha256>` in the
7
+ * blob store). `state` is `stored`, `deleting` (garbage collection in progress) or `purged`
8
+ * (takedown: the bytes are gone from every snapshot). An upload in flight holds `lease_until`,
9
+ * which keeps garbage collection away (§4.5).
10
+ * - snapshot_blobs: which content each snapshot references, so bytes a snapshot needs are never
11
+ * collected.
12
+ * - asset_refs: derived index of the live references (ImageRef.assetId and markdown `asset:<id>`)
13
+ * of the current pages, entries and terms; rewritten in the same batch as each node write.
14
+ * - media_uploads: single-use upload tickets for the REST stream (§5.5).
15
+ */
16
+ export declare const migration0013Media: Migration;
@@ -0,0 +1,69 @@
1
+
2
+
3
+ /**
4
+ * Assets and the media library (RFC-0004 §4.2). Asset nodes need no change of their own (`nodes.kind`
5
+ * is free text; their uniqueness key is the SHA-256). These tables hold what is not a node:
6
+ *
7
+ * - media_blobs: the ledger of content-addressed bytes per site (`media/<siteId>/<sha256>` in the
8
+ * blob store). `state` is `stored`, `deleting` (garbage collection in progress) or `purged`
9
+ * (takedown: the bytes are gone from every snapshot). An upload in flight holds `lease_until`,
10
+ * which keeps garbage collection away (§4.5).
11
+ * - snapshot_blobs: which content each snapshot references, so bytes a snapshot needs are never
12
+ * collected.
13
+ * - asset_refs: derived index of the live references (ImageRef.assetId and markdown `asset:<id>`)
14
+ * of the current pages, entries and terms; rewritten in the same batch as each node write.
15
+ * - media_uploads: single-use upload tickets for the REST stream (§5.5).
16
+ */
17
+ export const migration0013Media = {
18
+ name: "0013_media",
19
+ sql: `
20
+ CREATE TABLE media_blobs (
21
+ site_id TEXT NOT NULL,
22
+ sha256 TEXT NOT NULL,
23
+ bytes INTEGER NOT NULL,
24
+ media_type TEXT NOT NULL,
25
+ original_sha256 TEXT,
26
+ state TEXT NOT NULL,
27
+ lease_until TEXT,
28
+ purge_reason TEXT,
29
+ purge_legal INTEGER,
30
+ purged_by TEXT,
31
+ purged_at TEXT,
32
+ created_at TEXT NOT NULL,
33
+ updated_at TEXT NOT NULL,
34
+ PRIMARY KEY (site_id, sha256)
35
+ );
36
+ CREATE INDEX media_blobs_original ON media_blobs (site_id, original_sha256);
37
+ CREATE INDEX media_blobs_state ON media_blobs (site_id, state, created_at);
38
+ CREATE TABLE snapshot_blobs (
39
+ snapshot_id TEXT NOT NULL,
40
+ site_id TEXT NOT NULL,
41
+ sha256 TEXT NOT NULL,
42
+ PRIMARY KEY (snapshot_id, sha256)
43
+ );
44
+ CREATE INDEX snapshot_blobs_site_sha ON snapshot_blobs (site_id, sha256);
45
+ CREATE TABLE asset_refs (
46
+ site_id TEXT NOT NULL,
47
+ asset_id TEXT NOT NULL,
48
+ node_id TEXT NOT NULL,
49
+ node_kind TEXT NOT NULL,
50
+ field TEXT NOT NULL,
51
+ PRIMARY KEY (node_id, asset_id, field)
52
+ );
53
+ CREATE INDEX asset_refs_asset ON asset_refs (site_id, asset_id);
54
+ CREATE TABLE media_uploads (
55
+ id TEXT PRIMARY KEY,
56
+ site_id TEXT NOT NULL,
57
+ actor_kind TEXT NOT NULL,
58
+ actor_id TEXT NOT NULL,
59
+ sha256 TEXT,
60
+ bytes INTEGER NOT NULL,
61
+ media_type TEXT NOT NULL,
62
+ options TEXT NOT NULL,
63
+ state TEXT NOT NULL,
64
+ expires_at TEXT NOT NULL,
65
+ created_at TEXT NOT NULL
66
+ );
67
+ CREATE INDEX media_uploads_actor ON media_uploads (site_id, actor_id, state);
68
+ `,
69
+ };
@@ -0,0 +1,19 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * Site analytics (RFC-0006 §7.4). Additive; `@cloudhal/analytics` owns the accessors.
4
+ *
5
+ * - analytics_settings: per-site overrides of the deployment defaults, outside the site graph (a
6
+ * content rollback never changes a privacy setting). CAS on `rev`. A missing row = the default.
7
+ * - analytics_salts: one random salt per UTC day for the deployment (unique-visitors mode only);
8
+ * rows older than today are deleted hourly.
9
+ * - analytics_daily: daily rollups (UTC days). `dim` ∈ total | node | refHost | channel | utmSource
10
+ * | utmMedium | utmCampaign | country | device | browser | os | status | redirect | crawler |
11
+ * vitals.<metric>; key '(other)' holds what fell outside the top N, so totals add up. `good` and
12
+ * `poor` (vitals: weighted samples per rating) extend the RFC's table so Studio can show the share
13
+ * of good samples from rollups.
14
+ * - analytics_rollup_runs: which site-days are rolled up, written last, so the job is idempotent.
15
+ * - analytics_query_budget: uncached live (WAE) queries per UTC day (§7.7).
16
+ * - analytics_events: raw events for the SQLite store (local and Node self-host). Created everywhere
17
+ * because migrations are one ledger; never written on Cloudflare.
18
+ */
19
+ export declare const migration0014Analytics: Migration;
@@ -0,0 +1,105 @@
1
+
2
+
3
+ /**
4
+ * Site analytics (RFC-0006 §7.4). Additive; `@cloudhal/analytics` owns the accessors.
5
+ *
6
+ * - analytics_settings: per-site overrides of the deployment defaults, outside the site graph (a
7
+ * content rollback never changes a privacy setting). CAS on `rev`. A missing row = the default.
8
+ * - analytics_salts: one random salt per UTC day for the deployment (unique-visitors mode only);
9
+ * rows older than today are deleted hourly.
10
+ * - analytics_daily: daily rollups (UTC days). `dim` ∈ total | node | refHost | channel | utmSource
11
+ * | utmMedium | utmCampaign | country | device | browser | os | status | redirect | crawler |
12
+ * vitals.<metric>; key '(other)' holds what fell outside the top N, so totals add up. `good` and
13
+ * `poor` (vitals: weighted samples per rating) extend the RFC's table so Studio can show the share
14
+ * of good samples from rollups.
15
+ * - analytics_rollup_runs: which site-days are rolled up, written last, so the job is idempotent.
16
+ * - analytics_query_budget: uncached live (WAE) queries per UTC day (§7.7).
17
+ * - analytics_events: raw events for the SQLite store (local and Node self-host). Created everywhere
18
+ * because migrations are one ledger; never written on Cloudflare.
19
+ */
20
+ export const migration0014Analytics = {
21
+ name: "0014_analytics",
22
+ sql: `
23
+ CREATE TABLE analytics_settings (
24
+ site_id TEXT PRIMARY KEY,
25
+ enabled INTEGER NOT NULL,
26
+ unique_visitors INTEGER NOT NULL,
27
+ vitals_one_in INTEGER NOT NULL,
28
+ rollup_retention_days INTEGER,
29
+ plan_hint TEXT,
30
+ rev INTEGER NOT NULL,
31
+ updated_at TEXT NOT NULL,
32
+ updated_by TEXT NOT NULL
33
+ );
34
+ CREATE TABLE analytics_salts (
35
+ day TEXT PRIMARY KEY,
36
+ salt TEXT NOT NULL,
37
+ created_at TEXT NOT NULL
38
+ );
39
+ CREATE TABLE analytics_daily (
40
+ site_id TEXT NOT NULL,
41
+ day TEXT NOT NULL,
42
+ dim TEXT NOT NULL,
43
+ key TEXT NOT NULL,
44
+ pageviews INTEGER NOT NULL DEFAULT 0,
45
+ visits INTEGER NOT NULL DEFAULT 0,
46
+ visitors INTEGER,
47
+ hits INTEGER,
48
+ value REAL,
49
+ samples INTEGER,
50
+ good INTEGER,
51
+ poor INTEGER,
52
+ PRIMARY KEY (site_id, day, dim, key)
53
+ );
54
+ CREATE INDEX analytics_daily_key ON analytics_daily (site_id, dim, key, day);
55
+ CREATE TABLE analytics_rollup_runs (
56
+ site_id TEXT NOT NULL,
57
+ day TEXT NOT NULL,
58
+ state TEXT NOT NULL,
59
+ rows INTEGER,
60
+ source TEXT NOT NULL,
61
+ error TEXT,
62
+ finished_at TEXT,
63
+ PRIMARY KEY (site_id, day)
64
+ );
65
+ CREATE TABLE analytics_query_budget (
66
+ day TEXT PRIMARY KEY,
67
+ queries INTEGER NOT NULL
68
+ );
69
+ CREATE TABLE analytics_events (
70
+ id INTEGER PRIMARY KEY,
71
+ site_id TEXT NOT NULL,
72
+ ts TEXT NOT NULL,
73
+ kind TEXT NOT NULL,
74
+ path TEXT NOT NULL,
75
+ node_id TEXT,
76
+ node_kind TEXT,
77
+ ref_host TEXT,
78
+ ref_path TEXT,
79
+ channel TEXT,
80
+ utm_source TEXT,
81
+ utm_medium TEXT,
82
+ utm_campaign TEXT,
83
+ country TEXT,
84
+ device TEXT NOT NULL,
85
+ browser TEXT,
86
+ os TEXT,
87
+ visitor TEXT,
88
+ nav_type TEXT,
89
+ locale TEXT,
90
+ crawler TEXT,
91
+ redirect_to TEXT,
92
+ status INTEGER,
93
+ entrance INTEGER,
94
+ lcp INTEGER,
95
+ inp INTEGER,
96
+ cls REAL,
97
+ ttfb INTEGER,
98
+ fcp INTEGER,
99
+ render_ms INTEGER,
100
+ sample_one_in INTEGER NOT NULL DEFAULT 1
101
+ );
102
+ CREATE INDEX analytics_events_time ON analytics_events (site_id, kind, ts);
103
+ CREATE INDEX analytics_events_node ON analytics_events (site_id, node_id, ts);
104
+ `,
105
+ };
@@ -0,0 +1,8 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * The privacy signal of a page view (RFC-0006 §6.2, `AnalyticsPageviewEvent.privacySignal`): 1 when
4
+ * the beacon request carried GPC or DNT, so Studio can say how many page views were counted without
5
+ * a visitor hash. Additive: older rows read as NULL (no signal recorded). Only `analytics_events`
6
+ * (the SQLite store) needs a column; daily rollups keep the count in a `signals` row.
7
+ */
8
+ export declare const migration0015AnalyticsPrivacySignal: Migration;
@@ -0,0 +1,14 @@
1
+
2
+
3
+ /**
4
+ * The privacy signal of a page view (RFC-0006 §6.2, `AnalyticsPageviewEvent.privacySignal`): 1 when
5
+ * the beacon request carried GPC or DNT, so Studio can say how many page views were counted without
6
+ * a visitor hash. Additive: older rows read as NULL (no signal recorded). Only `analytics_events`
7
+ * (the SQLite store) needs a column; daily rollups keep the count in a `signals` row.
8
+ */
9
+ export const migration0015AnalyticsPrivacySignal = {
10
+ name: "0015_analytics_privacy_signal",
11
+ sql: `
12
+ ALTER TABLE analytics_events ADD COLUMN privacy_signal INTEGER;
13
+ `,
14
+ };
@@ -0,0 +1,27 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * Reader comments (RFC-0007 §4.1). Additive; `@cloudhal/comments` owns the accessors. Comments
4
+ * live outside the site graph: snapshots, diffs, rollbacks and bundles never read these tables.
5
+ *
6
+ * - comment_settings: per-site settings (a missing row = off), collections inside as JSON. CAS on `rev`.
7
+ * - comment_secrets: owner-supplied keys (the Turnstile secret), AES-GCM under a key derived from
8
+ * CLOUDHAL_COMMENTS_KEY; write-only through the API.
9
+ * - comment_threads: per-entry overrides (a missing row = default). CAS on `rev`.
10
+ * - comments: one row per comment, keyed by entry id. `body` is plain text (NULL once deleted);
11
+ * `email_id`, `ip_hmac` and `challenge_id` never leave the store (they are not in the contract).
12
+ * `hash` is the SHA-256 of the canonical Comment record; `op` the last bulk operation, which
13
+ * guards the statements that follow it in the same batch. No counter columns: counts are
14
+ * indexed COUNTs on read.
15
+ * - commenter_emails: the only place an address lives (encrypted, plus a keyed hash for lookups).
16
+ * - comment_events: moderation history and statements of reasons (no text, no email).
17
+ * - comment_reports: notices (DSA Art. 16); a report never changes a comment by itself.
18
+ * - comment_bans: keyed hashes, never addresses.
19
+ * - comment_salts: one random salt per UTC month for IP HMACs; deleted when two months old.
20
+ * - comment_purge_queue: cache tags waiting for a coalesced purge.
21
+ * - cache_purge_log: purge requests sent by this deployment (publish and comments), for the
22
+ * shared Free-tier purge budget.
23
+ *
24
+ * Beyond the RFC's listing: `comments_parent` (replies and tombstone checks) and
25
+ * `comment_reports_comment` (a comment's reports) indexes, both partial or small.
26
+ */
27
+ export declare const migration0016Comments: Migration;
@@ -0,0 +1,173 @@
1
+
2
+
3
+ /**
4
+ * Reader comments (RFC-0007 §4.1). Additive; `@cloudhal/comments` owns the accessors. Comments
5
+ * live outside the site graph: snapshots, diffs, rollbacks and bundles never read these tables.
6
+ *
7
+ * - comment_settings: per-site settings (a missing row = off), collections inside as JSON. CAS on `rev`.
8
+ * - comment_secrets: owner-supplied keys (the Turnstile secret), AES-GCM under a key derived from
9
+ * CLOUDHAL_COMMENTS_KEY; write-only through the API.
10
+ * - comment_threads: per-entry overrides (a missing row = default). CAS on `rev`.
11
+ * - comments: one row per comment, keyed by entry id. `body` is plain text (NULL once deleted);
12
+ * `email_id`, `ip_hmac` and `challenge_id` never leave the store (they are not in the contract).
13
+ * `hash` is the SHA-256 of the canonical Comment record; `op` the last bulk operation, which
14
+ * guards the statements that follow it in the same batch. No counter columns: counts are
15
+ * indexed COUNTs on read.
16
+ * - commenter_emails: the only place an address lives (encrypted, plus a keyed hash for lookups).
17
+ * - comment_events: moderation history and statements of reasons (no text, no email).
18
+ * - comment_reports: notices (DSA Art. 16); a report never changes a comment by itself.
19
+ * - comment_bans: keyed hashes, never addresses.
20
+ * - comment_salts: one random salt per UTC month for IP HMACs; deleted when two months old.
21
+ * - comment_purge_queue: cache tags waiting for a coalesced purge.
22
+ * - cache_purge_log: purge requests sent by this deployment (publish and comments), for the
23
+ * shared Free-tier purge budget.
24
+ *
25
+ * Beyond the RFC's listing: `comments_parent` (replies and tombstone checks) and
26
+ * `comment_reports_comment` (a comment's reports) indexes, both partial or small.
27
+ */
28
+ export const migration0016Comments = {
29
+ name: "0016_comments",
30
+ sql: `
31
+ CREATE TABLE comment_settings (
32
+ site_id TEXT PRIMARY KEY,
33
+ enabled INTEGER NOT NULL,
34
+ show_when_off INTEGER NOT NULL,
35
+ collections TEXT NOT NULL,
36
+ close_after_days INTEGER,
37
+ protection TEXT NOT NULL,
38
+ turnstile_site_key TEXT,
39
+ allow_without_script INTEGER NOT NULL,
40
+ link_limit INTEGER NOT NULL,
41
+ watch_words TEXT NOT NULL,
42
+ blocked_words TEXT NOT NULL,
43
+ daily_cap INTEGER NOT NULL,
44
+ ai_labels TEXT NOT NULL,
45
+ retention TEXT NOT NULL,
46
+ plan_hint TEXT,
47
+ rev INTEGER NOT NULL,
48
+ updated_at TEXT NOT NULL,
49
+ updated_by TEXT NOT NULL
50
+ );
51
+ CREATE TABLE comment_secrets (
52
+ site_id TEXT NOT NULL,
53
+ name TEXT NOT NULL,
54
+ ciphertext TEXT NOT NULL,
55
+ iv TEXT NOT NULL,
56
+ updated_at TEXT NOT NULL,
57
+ updated_by TEXT NOT NULL,
58
+ PRIMARY KEY (site_id, name)
59
+ );
60
+ CREATE TABLE comment_threads (
61
+ site_id TEXT NOT NULL,
62
+ node_id TEXT NOT NULL,
63
+ mode TEXT NOT NULL,
64
+ closes_at TEXT,
65
+ rev INTEGER NOT NULL,
66
+ updated_at TEXT NOT NULL,
67
+ updated_by TEXT NOT NULL,
68
+ PRIMARY KEY (site_id, node_id)
69
+ );
70
+ CREATE TABLE comments (
71
+ id TEXT PRIMARY KEY,
72
+ site_id TEXT NOT NULL,
73
+ node_id TEXT NOT NULL,
74
+ parent_id TEXT,
75
+ in_reply_to_id TEXT,
76
+ source TEXT NOT NULL,
77
+ origin TEXT,
78
+ author TEXT NOT NULL,
79
+ body TEXT,
80
+ status TEXT NOT NULL,
81
+ labels TEXT NOT NULL,
82
+ moderation TEXT,
83
+ email_id TEXT,
84
+ ip_hmac TEXT,
85
+ challenge_id TEXT,
86
+ proof TEXT NOT NULL,
87
+ op TEXT,
88
+ hash TEXT NOT NULL,
89
+ created_at TEXT NOT NULL,
90
+ edited_at TEXT,
91
+ rev INTEGER NOT NULL
92
+ );
93
+ CREATE INDEX comments_node ON comments (site_id, node_id, status, created_at);
94
+ CREATE INDEX comments_queue ON comments (site_id, status, created_at);
95
+ CREATE INDEX comments_client ON comments (site_id, ip_hmac, created_at) WHERE ip_hmac IS NOT NULL;
96
+ CREATE UNIQUE INDEX comments_challenge ON comments (challenge_id) WHERE challenge_id IS NOT NULL;
97
+ CREATE INDEX comments_email ON comments (email_id) WHERE email_id IS NOT NULL;
98
+ CREATE INDEX comments_parent ON comments (parent_id) WHERE parent_id IS NOT NULL;
99
+ CREATE TABLE commenter_emails (
100
+ id TEXT PRIMARY KEY,
101
+ site_id TEXT NOT NULL,
102
+ email_hmac TEXT NOT NULL,
103
+ email_ciphertext TEXT NOT NULL,
104
+ email_iv TEXT NOT NULL,
105
+ created_at TEXT NOT NULL,
106
+ UNIQUE (site_id, email_hmac)
107
+ );
108
+ CREATE TABLE comment_events (
109
+ id TEXT PRIMARY KEY,
110
+ site_id TEXT NOT NULL,
111
+ comment_id TEXT NOT NULL,
112
+ action TEXT NOT NULL,
113
+ actor TEXT NOT NULL,
114
+ reason_code TEXT,
115
+ note TEXT,
116
+ automated INTEGER NOT NULL,
117
+ approval_id TEXT,
118
+ op TEXT,
119
+ at TEXT NOT NULL
120
+ );
121
+ CREATE INDEX comment_events_comment ON comment_events (comment_id, at);
122
+ CREATE TABLE comment_reports (
123
+ id TEXT PRIMARY KEY,
124
+ site_id TEXT NOT NULL,
125
+ comment_id TEXT NOT NULL,
126
+ reason TEXT NOT NULL,
127
+ details TEXT,
128
+ email_id TEXT,
129
+ ip_hmac TEXT,
130
+ challenge_id TEXT,
131
+ created_at TEXT NOT NULL,
132
+ resolved_at TEXT,
133
+ resolved_by TEXT,
134
+ resolution TEXT
135
+ );
136
+ CREATE INDEX comment_reports_open ON comment_reports (site_id, resolved_at, created_at);
137
+ CREATE UNIQUE INDEX comment_reports_challenge ON comment_reports (challenge_id) WHERE challenge_id IS NOT NULL;
138
+ CREATE INDEX comment_reports_comment ON comment_reports (comment_id);
139
+ CREATE TABLE comment_bans (
140
+ id TEXT PRIMARY KEY,
141
+ site_id TEXT NOT NULL,
142
+ subject TEXT NOT NULL,
143
+ value TEXT NOT NULL,
144
+ until TEXT,
145
+ reason_code TEXT NOT NULL,
146
+ note TEXT,
147
+ by_actor TEXT NOT NULL,
148
+ at TEXT NOT NULL,
149
+ UNIQUE (site_id, subject, value)
150
+ );
151
+ CREATE TABLE comment_salts (
152
+ month TEXT PRIMARY KEY,
153
+ salt TEXT NOT NULL,
154
+ created_at TEXT NOT NULL
155
+ );
156
+ CREATE TABLE comment_purge_queue (
157
+ site_id TEXT NOT NULL,
158
+ tag TEXT NOT NULL,
159
+ priority TEXT NOT NULL,
160
+ enqueued_at TEXT NOT NULL,
161
+ attempts INTEGER NOT NULL,
162
+ next_at TEXT NOT NULL,
163
+ PRIMARY KEY (site_id, tag)
164
+ );
165
+ CREATE TABLE cache_purge_log (
166
+ id INTEGER PRIMARY KEY,
167
+ at TEXT NOT NULL,
168
+ source TEXT NOT NULL,
169
+ requests INTEGER NOT NULL
170
+ );
171
+ CREATE INDEX cache_purge_log_at ON cache_purge_log (at);
172
+ `,
173
+ };
@@ -0,0 +1,21 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * Schedules (RFC-0008 §5.1). Additive: new tables, nullable columns on `audit` and `approvals`;
4
+ * `@cloudhal/schedules` owns the accessors (`SqlScheduleStore`). Releases, automations and runs
5
+ * live outside the site graph: snapshots, diffs, rollbacks and bundles never read these tables.
6
+ *
7
+ * - releases / release_items: a release and its pinned items. The tick looks only at
8
+ * `releases_due`; `release_items.held` mirrors "the release is awaiting_approval, scheduled or
9
+ * running" and drives locks and holds (`release_items_held`).
10
+ * - automations: recurring jobs and their per-automation key (`automations_key`, unique).
11
+ * - schedule_runs: one row per occurrence, keyed by a deterministic id (the idempotency key of
12
+ * every scheduled effect); `schedule_runs_open` finds windows and leases to close.
13
+ * - system_jobs: watermarks of the deployment's periodic jobs (analytics rollup, comments, …).
14
+ * - scheduler_heartbeat: one row — is the scheduler alive, when does it expect to wake next.
15
+ * - schedule_settings: per-site kill switch and daily automation write cap, outside the graph so
16
+ * a rollback cannot un-pause a site. A missing row means the defaults.
17
+ * - audit and approvals gain the run, release and automation they belong to.
18
+ *
19
+ * No counter columns: budget usage and daily caps are indexed COUNTs over audit rows.
20
+ */
21
+ export declare const migration0017Schedules: Migration;
@@ -0,0 +1,108 @@
1
+
2
+
3
+ /**
4
+ * Schedules (RFC-0008 §5.1). Additive: new tables, nullable columns on `audit` and `approvals`;
5
+ * `@cloudhal/schedules` owns the accessors (`SqlScheduleStore`). Releases, automations and runs
6
+ * live outside the site graph: snapshots, diffs, rollbacks and bundles never read these tables.
7
+ *
8
+ * - releases / release_items: a release and its pinned items. The tick looks only at
9
+ * `releases_due`; `release_items.held` mirrors "the release is awaiting_approval, scheduled or
10
+ * running" and drives locks and holds (`release_items_held`).
11
+ * - automations: recurring jobs and their per-automation key (`automations_key`, unique).
12
+ * - schedule_runs: one row per occurrence, keyed by a deterministic id (the idempotency key of
13
+ * every scheduled effect); `schedule_runs_open` finds windows and leases to close.
14
+ * - system_jobs: watermarks of the deployment's periodic jobs (analytics rollup, comments, …).
15
+ * - scheduler_heartbeat: one row — is the scheduler alive, when does it expect to wake next.
16
+ * - schedule_settings: per-site kill switch and daily automation write cap, outside the graph so
17
+ * a rollback cannot un-pause a site. A missing row means the defaults.
18
+ * - audit and approvals gain the run, release and automation they belong to.
19
+ *
20
+ * No counter columns: budget usage and daily caps are indexed COUNTs over audit rows.
21
+ */
22
+ export const migration0017Schedules = {
23
+ name: "0017_schedules",
24
+ sql: `
25
+ CREATE TABLE releases (
26
+ id TEXT PRIMARY KEY, site_id TEXT NOT NULL, title TEXT,
27
+ state TEXT NOT NULL,
28
+ hash TEXT NOT NULL,
29
+ when_kind TEXT,
30
+ execute_at TEXT,
31
+ time_zone TEXT, local_time TEXT,
32
+ approval TEXT,
33
+ request_id TEXT,
34
+ outcome TEXT,
35
+ purge_plan TEXT,
36
+ lease_token TEXT, lease_until TEXT,
37
+ created_by TEXT NOT NULL,
38
+ metadata TEXT, rev INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
39
+ );
40
+ CREATE INDEX releases_due ON releases (execute_at) WHERE state = 'scheduled';
41
+ CREATE INDEX releases_site ON releases (site_id, state, execute_at);
42
+ CREATE TABLE release_items (
43
+ release_id TEXT NOT NULL, site_id TEXT NOT NULL, node_id TEXT NOT NULL,
44
+ op TEXT NOT NULL,
45
+ node_kind TEXT NOT NULL,
46
+ target_rev INTEGER, target_hash TEXT,
47
+ base_rev INTEGER, base_hash TEXT,
48
+ deps TEXT NOT NULL,
49
+ blob_sha TEXT,
50
+ held INTEGER NOT NULL,
51
+ PRIMARY KEY (release_id, node_id)
52
+ );
53
+ CREATE INDEX release_items_held ON release_items (site_id, node_id) WHERE held = 1;
54
+ CREATE TABLE automations (
55
+ id TEXT PRIMARY KEY, site_id TEXT NOT NULL, name TEXT NOT NULL,
56
+ job TEXT NOT NULL,
57
+ key_id TEXT,
58
+ start_local TEXT NOT NULL, time_zone TEXT NOT NULL, rule TEXT, preset TEXT NOT NULL,
59
+ state TEXT NOT NULL, paused_reason TEXT, consecutive_failures INTEGER NOT NULL,
60
+ owner_id TEXT NOT NULL, owner_name TEXT,
61
+ next_run_at TEXT,
62
+ cursor TEXT,
63
+ lease_token TEXT, lease_until TEXT,
64
+ metadata TEXT, rev INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
65
+ );
66
+ CREATE INDEX automations_due ON automations (next_run_at) WHERE state = 'active';
67
+ CREATE INDEX automations_site ON automations (site_id, state);
68
+ CREATE UNIQUE INDEX automations_key ON automations (key_id) WHERE key_id IS NOT NULL;
69
+ CREATE TABLE schedule_runs (
70
+ id TEXT PRIMARY KEY,
71
+ site_id TEXT NOT NULL, subject_kind TEXT NOT NULL, subject_id TEXT NOT NULL,
72
+ occurrence_at TEXT NOT NULL,
73
+ state TEXT NOT NULL, outcome TEXT, trigger TEXT, attempt INTEGER NOT NULL,
74
+ lease_token TEXT, lease_until TEXT,
75
+ window_opens TEXT, window_closes TEXT,
76
+ started_at TEXT, ended_at TEXT, lateness_s INTEGER, adjusted TEXT,
77
+ usage TEXT, taint TEXT, summary TEXT, counts TEXT, findings TEXT, approval_ids TEXT, error TEXT,
78
+ rev INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
79
+ UNIQUE (subject_id, occurrence_at)
80
+ );
81
+ CREATE INDEX schedule_runs_site ON schedule_runs (site_id, created_at);
82
+ CREATE INDEX schedule_runs_open ON schedule_runs (window_closes) WHERE state IN ('ready', 'running');
83
+ CREATE TABLE system_jobs (
84
+ name TEXT PRIMARY KEY,
85
+ next_run_at TEXT NOT NULL, last_run_at TEXT, last_ok_at TEXT,
86
+ lease_token TEXT, lease_until TEXT, last_error TEXT
87
+ );
88
+ CREATE TABLE scheduler_heartbeat (
89
+ id INTEGER PRIMARY KEY CHECK (id = 1),
90
+ at TEXT NOT NULL, trigger TEXT NOT NULL, next_wake_at TEXT NOT NULL,
91
+ ran INTEGER NOT NULL, more INTEGER NOT NULL, lag_s INTEGER
92
+ );
93
+ CREATE TABLE schedule_settings (
94
+ site_id TEXT PRIMARY KEY,
95
+ paused INTEGER NOT NULL,
96
+ paused_by TEXT, paused_at TEXT, paused_reason TEXT,
97
+ daily_write_cap INTEGER NOT NULL,
98
+ rev INTEGER NOT NULL, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL
99
+ );
100
+ ALTER TABLE audit ADD COLUMN run_id TEXT;
101
+ ALTER TABLE audit ADD COLUMN release_id TEXT;
102
+ ALTER TABLE audit ADD COLUMN automation_id TEXT;
103
+ CREATE INDEX audit_run ON audit (run_id, at) WHERE run_id IS NOT NULL;
104
+ CREATE INDEX audit_automation_day ON audit (automation_id, at) WHERE automation_id IS NOT NULL;
105
+ ALTER TABLE approvals ADD COLUMN release_id TEXT;
106
+ ALTER TABLE approvals ADD COLUMN run_id TEXT;
107
+ `,
108
+ };
@@ -0,0 +1,12 @@
1
+ import type { Migration } from "../src/migrate.js";
2
+ /**
3
+ * Indexes for queries that scanned `nodes` or `media_uploads` (D1 bills rows read):
4
+ * - `nodes_sites`: `listSites()` (`WHERE kind = 'site' ORDER BY created_at, id`), which Studio's
5
+ * notification poll runs, scanned every node of every site;
6
+ * - `nodes_site_kind_created`: per-site lists by kind in creation order (pages, media library)
7
+ * sorted in a temporary B-tree. It covers `(site_id, kind)`, so `nodes_site_kind` goes;
8
+ * - `nodes_assets_created`: a key's daily upload limit (`created_at >= ?` over assets);
9
+ * - `media_uploads_expires`: removing expired upload tickets.
10
+ * Index-only: no table or row changes.
11
+ */
12
+ export declare const migration0018QueryIndexes: Migration;