@saacms/core 0.1.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.
Files changed (116) hide show
  1. package/README.md +25 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/access/index.d.ts +37 -0
  4. package/dist/access/index.d.ts.map +1 -0
  5. package/dist/access/index.js +6 -0
  6. package/dist/auth/index.d.ts +30 -0
  7. package/dist/auth/index.d.ts.map +1 -0
  8. package/dist/codegen/content-migration.d.ts +167 -0
  9. package/dist/codegen/content-migration.d.ts.map +1 -0
  10. package/dist/codegen/filter-openapi-for-user.d.ts +100 -0
  11. package/dist/codegen/filter-openapi-for-user.d.ts.map +1 -0
  12. package/dist/codegen/index.d.ts +19 -0
  13. package/dist/codegen/index.d.ts.map +1 -0
  14. package/dist/codegen/index.js +43 -0
  15. package/dist/codegen/openapi-types.d.ts +125 -0
  16. package/dist/codegen/openapi-types.d.ts.map +1 -0
  17. package/dist/codegen/to-d1-migration.d.ts +88 -0
  18. package/dist/codegen/to-d1-migration.d.ts.map +1 -0
  19. package/dist/codegen/to-drizzle.d.ts +131 -0
  20. package/dist/codegen/to-drizzle.d.ts.map +1 -0
  21. package/dist/codegen/to-openapi.d.ts +80 -0
  22. package/dist/codegen/to-openapi.d.ts.map +1 -0
  23. package/dist/codegen/to-puck-fields.d.ts +109 -0
  24. package/dist/codegen/to-puck-fields.d.ts.map +1 -0
  25. package/dist/codegen/to-ts-types.d.ts +59 -0
  26. package/dist/codegen/to-ts-types.d.ts.map +1 -0
  27. package/dist/hooks/index.d.ts +94 -0
  28. package/dist/hooks/index.d.ts.map +1 -0
  29. package/dist/hooks/index.js +8 -0
  30. package/dist/host/index.d.ts +109 -0
  31. package/dist/host/index.d.ts.map +1 -0
  32. package/dist/host/index.js +16 -0
  33. package/dist/index-172n82sz.js +4 -0
  34. package/dist/index-8g8ymd37.js +275 -0
  35. package/dist/index-a3pnt8yz.js +1494 -0
  36. package/dist/index-b59hfany.js +3078 -0
  37. package/dist/index-b7z43xwp.js +6 -0
  38. package/dist/index-r0at8zaw.js +13 -0
  39. package/dist/index-zgbq60fy.js +74 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +261 -0
  43. package/dist/observability/audit.d.ts +65 -0
  44. package/dist/observability/audit.d.ts.map +1 -0
  45. package/dist/observability/index.d.ts +2 -0
  46. package/dist/observability/index.d.ts.map +1 -0
  47. package/dist/publish/compile.d.ts +76 -0
  48. package/dist/publish/compile.d.ts.map +1 -0
  49. package/dist/runtime/auth-middleware.d.ts +34 -0
  50. package/dist/runtime/auth-middleware.d.ts.map +1 -0
  51. package/dist/runtime/boolean-columns.d.ts +65 -0
  52. package/dist/runtime/boolean-columns.d.ts.map +1 -0
  53. package/dist/runtime/cache.d.ts +62 -0
  54. package/dist/runtime/cache.d.ts.map +1 -0
  55. package/dist/runtime/create-route.d.ts +26 -0
  56. package/dist/runtime/create-route.d.ts.map +1 -0
  57. package/dist/runtime/delete-route.d.ts +15 -0
  58. package/dist/runtime/delete-route.d.ts.map +1 -0
  59. package/dist/runtime/drafts-route.d.ts +65 -0
  60. package/dist/runtime/drafts-route.d.ts.map +1 -0
  61. package/dist/runtime/health-route.d.ts +23 -0
  62. package/dist/runtime/health-route.d.ts.map +1 -0
  63. package/dist/runtime/index.d.ts +24 -0
  64. package/dist/runtime/index.d.ts.map +1 -0
  65. package/dist/runtime/index.js +17 -0
  66. package/dist/runtime/json-columns.d.ts +50 -0
  67. package/dist/runtime/json-columns.d.ts.map +1 -0
  68. package/dist/runtime/list-route.d.ts +20 -0
  69. package/dist/runtime/list-route.d.ts.map +1 -0
  70. package/dist/runtime/openapi-route.d.ts +30 -0
  71. package/dist/runtime/openapi-route.d.ts.map +1 -0
  72. package/dist/runtime/pattern-route.d.ts +48 -0
  73. package/dist/runtime/pattern-route.d.ts.map +1 -0
  74. package/dist/runtime/problem-details.d.ts +32 -0
  75. package/dist/runtime/problem-details.d.ts.map +1 -0
  76. package/dist/runtime/put-route.d.ts +19 -0
  77. package/dist/runtime/put-route.d.ts.map +1 -0
  78. package/dist/runtime/read-route.d.ts +26 -0
  79. package/dist/runtime/read-route.d.ts.map +1 -0
  80. package/dist/runtime/scale-cost.d.ts +84 -0
  81. package/dist/runtime/scale-cost.d.ts.map +1 -0
  82. package/dist/runtime/scheme-route.d.ts +49 -0
  83. package/dist/runtime/scheme-route.d.ts.map +1 -0
  84. package/dist/runtime/services.d.ts +42 -0
  85. package/dist/runtime/services.d.ts.map +1 -0
  86. package/dist/runtime/update-route.d.ts +20 -0
  87. package/dist/runtime/update-route.d.ts.map +1 -0
  88. package/dist/runtime/upload-route.d.ts +46 -0
  89. package/dist/runtime/upload-route.d.ts.map +1 -0
  90. package/dist/schema/index.d.ts +185 -0
  91. package/dist/schema/index.d.ts.map +1 -0
  92. package/dist/schema/index.js +40 -0
  93. package/dist/schema/media.d.ts +237 -0
  94. package/dist/schema/media.d.ts.map +1 -0
  95. package/dist/schema/plugin-trust.d.ts +144 -0
  96. package/dist/schema/plugin-trust.d.ts.map +1 -0
  97. package/dist/signals/index.d.ts +10 -0
  98. package/dist/signals/index.d.ts.map +1 -0
  99. package/dist/signals/index.js +10 -0
  100. package/dist/storage/index.d.ts +120 -0
  101. package/dist/storage/index.d.ts.map +1 -0
  102. package/dist/storage/index.js +13 -0
  103. package/dist/tenant/index.d.ts +105 -0
  104. package/dist/tenant/index.d.ts.map +1 -0
  105. package/dist/theme/index.d.ts +56 -0
  106. package/dist/theme/index.d.ts.map +1 -0
  107. package/dist/types/ids.d.ts +45 -0
  108. package/dist/types/ids.d.ts.map +1 -0
  109. package/dist/types/ids.js +15 -0
  110. package/dist/types/index.d.ts +3 -0
  111. package/dist/types/index.d.ts.map +1 -0
  112. package/dist/types/index.js +6 -0
  113. package/dist/types/user.d.ts +14 -0
  114. package/dist/types/user.d.ts.map +1 -0
  115. package/dist/types/user.js +1 -0
  116. package/package.json +116 -0
@@ -0,0 +1,6 @@
1
+ // src/access/index.ts
2
+ function evaluateAccess(_predicate, _ctx) {
3
+ throw new Error("NOT_IMPLEMENTED: evaluateAccess");
4
+ }
5
+
6
+ export { evaluateAccess };
@@ -0,0 +1,13 @@
1
+ // src/types/ids.ts
2
+ var id = {
3
+ user: (s) => s,
4
+ session: (s) => s,
5
+ collection: (s) => s,
6
+ record: (s) => s,
7
+ page: (s) => s,
8
+ block: (s) => s,
9
+ draft: (s) => s,
10
+ publication: (s) => s,
11
+ migration: (s) => s
12
+ };
13
+ export { id };
@@ -0,0 +1,74 @@
1
+ // src/hooks/index.ts
2
+ import { Effect } from "effect";
3
+ var COMMAND_PHASE_MOMENTS = new Set([
4
+ "beforeValidate",
5
+ "beforeChange",
6
+ "beforeRead",
7
+ "beforeDelete",
8
+ "beforePublish",
9
+ "beforeRender"
10
+ ]);
11
+ function isCommandPhase(moment) {
12
+ return COMMAND_PHASE_MOMENTS.has(moment);
13
+ }
14
+ function isPlainObject(value) {
15
+ return typeof value === "object" && value !== null;
16
+ }
17
+ function runHooks(moment, ctx, hooks) {
18
+ const command = isCommandPhase(moment);
19
+ if (hooks.length === 0) {
20
+ return command ? Effect.succeed(ctx.data) : Effect.void;
21
+ }
22
+ return command ? runCommandPhase(ctx, hooks) : runEventPhase(moment, ctx, hooks);
23
+ }
24
+ function runCommandPhase(initialCtx, hooks) {
25
+ const seed = Effect.succeed({
26
+ currentData: initialCtx.data,
27
+ lastResult: initialCtx.data
28
+ });
29
+ const chained = hooks.reduce((acc, hook) => Effect.flatMap(acc, (state) => {
30
+ const hookCtx = {
31
+ ...initialCtx,
32
+ data: state.currentData
33
+ };
34
+ return Effect.map(hook.fn(hookCtx), (out) => {
35
+ if (isPlainObject(out)) {
36
+ return { currentData: out, lastResult: out };
37
+ }
38
+ return state;
39
+ });
40
+ }), seed);
41
+ return Effect.map(chained, (state) => state.lastResult);
42
+ }
43
+ function runEventPhase(moment, ctx, hooks) {
44
+ const effects = hooks.map((hook) => {
45
+ const effect = hook.fn(ctx);
46
+ if (hook.options?.failOnError === true) {
47
+ return effect;
48
+ }
49
+ return Effect.catchAll(effect, (err) => Effect.sync(() => {
50
+ console.warn(`[saacms/hooks] ${moment} hook failed:`, err);
51
+ }));
52
+ });
53
+ return Effect.all(effects, { concurrency: "unbounded", discard: true });
54
+ }
55
+ function resolveHooksFor(moment, source) {
56
+ const result = [];
57
+ appendEntry(source.collection?.hooks?.[moment], result);
58
+ for (const plugin of source.plugins ?? []) {
59
+ appendEntry(plugin.hooks?.[moment], result);
60
+ }
61
+ return result;
62
+ }
63
+ function appendEntry(entry, result) {
64
+ if (typeof entry === "function") {
65
+ result.push({ fn: entry });
66
+ return;
67
+ }
68
+ if (isPlainObject(entry) && typeof entry.fn === "function") {
69
+ const e = entry;
70
+ result.push(e.options !== undefined ? { fn: e.fn, options: e.options } : { fn: e.fn });
71
+ }
72
+ }
73
+
74
+ export { runHooks, resolveHooksFor };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @saacms/core — public surface.
3
+ *
4
+ * Re-exports the canonical types and (when implemented) the runtime handler,
5
+ * schema projections, hook orchestrator, signals, and SSE primitives.
6
+ *
7
+ * v1 alpha status: types are stable; implementations are stubs.
8
+ */
9
+ export * from "./types/index.ts";
10
+ export * from "./schema/index.ts";
11
+ export * from "./theme/index.ts";
12
+ export * from "./access/index.ts";
13
+ export * from "./auth/index.ts";
14
+ export * from "./hooks/index.ts";
15
+ export * from "./runtime/index.ts";
16
+ export * from "./signals/index.ts";
17
+ export * from "./storage/index.ts";
18
+ export * from "./host/index.ts";
19
+ export * from "./codegen/index.ts";
20
+ export * from "./publish/compile.ts";
21
+ export * from "./tenant/index.ts";
22
+ export * from "./observability/index.ts";
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,0BAA0B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,261 @@
1
+ import {
2
+ evaluateAccess
3
+ } from "./index-b7z43xwp.js";
4
+ import {
5
+ DEFAULT_THRESHOLDS,
6
+ RowStorageError,
7
+ UniqueConstraintError,
8
+ collectServices,
9
+ computeScaleCostSignal,
10
+ createSaacmsRuntime,
11
+ getServices,
12
+ recordAuditEvent
13
+ } from "./index-b59hfany.js";
14
+ import {
15
+ resolveHooksFor,
16
+ runHooks
17
+ } from "./index-zgbq60fy.js";
18
+ import {
19
+ computed,
20
+ effect,
21
+ signal
22
+ } from "./index-172n82sz.js";
23
+ import {
24
+ id
25
+ } from "./index-r0at8zaw.js";
26
+ import {
27
+ assertNoTableNameCollisions,
28
+ camelToSnakeCase,
29
+ collectionToOpenApiPaths,
30
+ contentMigrationModuleSource,
31
+ defaultLabel,
32
+ detectPendingContentMigrations,
33
+ diffBlockSchemas,
34
+ filterOpenApiForUser,
35
+ fingerprintBlockSchema,
36
+ fingerprintToString,
37
+ normalizeDateSchemas,
38
+ schemaToD1Migration,
39
+ schemaToD1Migrations,
40
+ schemaToDrizzle,
41
+ schemaToOpenApiSchema,
42
+ schemaToPuckFields,
43
+ schemaToTsType,
44
+ schemaToTsTypes,
45
+ slugToTableName
46
+ } from "./index-a3pnt8yz.js";
47
+ import {
48
+ MEDIA_FIELD_NAMES,
49
+ MediaFields,
50
+ MediaRef,
51
+ PLUGIN_CAPABILITIES,
52
+ REPO_MEDIA_FIELD_NAMES,
53
+ assertPluginCapabilities,
54
+ defineBlock,
55
+ defineCollection,
56
+ defineConfig,
57
+ definePage,
58
+ definePageTemplate,
59
+ definePlugin,
60
+ deriveContributedCapabilities,
61
+ mediaRepoName,
62
+ mediaTransformUrl,
63
+ resolveMediaRef,
64
+ stageRepoMedia,
65
+ withMediaFields
66
+ } from "./index-8g8ymd37.js";
67
+ // src/theme/index.ts
68
+ function defineTheme(opts) {
69
+ return opts;
70
+ }
71
+ function themeFingerprint(schemeId, darkMode, tenantScope) {
72
+ const schemeSegment = schemeId != null ? `id:${schemeId}` : "none";
73
+ const darkSegment = darkMode ? "dark" : "light";
74
+ const tenantSegment = tenantScope != null ? `:t:${tenantScope}` : "";
75
+ return `scheme:${schemeSegment}:${darkSegment}${tenantSegment}`;
76
+ }
77
+ // src/host/index.ts
78
+ var SCHEME_COOKIE = "saacms-scheme";
79
+ var DARK_MODE_COOKIE = "saacms-dark";
80
+ // src/publish/compile.ts
81
+ import { mkdirSync, writeFileSync } from "node:fs";
82
+ import { dirname, resolve } from "node:path";
83
+ function isCompileAdapter(host) {
84
+ if (typeof host !== "object" || host === null)
85
+ return false;
86
+ const h = host;
87
+ return typeof h.assetRoot === "function" && typeof h.generateRoute === "function";
88
+ }
89
+ async function compileForPublish(config, opts) {
90
+ const { cwd, dryRun = false } = opts;
91
+ const routes = [];
92
+ const content = [];
93
+ const media = [];
94
+ const adapter = isCompileAdapter(config.host) ? config.host : null;
95
+ for (const page of config.pages ?? []) {
96
+ const pageId = String(page.id);
97
+ const pageLayout = opts.readPageContent ? opts.readPageContent(pageId) : page.layout;
98
+ const contentRelPath = `content/pages/${pageId}.json`;
99
+ if (!dryRun) {
100
+ writeFileSafe(resolve(cwd, contentRelPath), cwd, JSON.stringify({ id: pageId, path: page.path, layout: pageLayout }, null, 2));
101
+ }
102
+ content.push(contentRelPath);
103
+ if (adapter !== null) {
104
+ const rawPath = String(page.path);
105
+ const url = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
106
+ const generated = adapter.generateRoute({
107
+ id: pageId,
108
+ url,
109
+ renderMode: page.renderMode ?? "static",
110
+ layout: pageLayout
111
+ }, {});
112
+ if (!dryRun) {
113
+ writeFileSafe(resolve(cwd, generated.path), cwd, generated.source);
114
+ }
115
+ routes.push(generated.path);
116
+ }
117
+ }
118
+ const assetRoot = adapter?.assetRoot() ?? "public/";
119
+ for (const coll of config.collections ?? []) {
120
+ if (coll.kind !== "media")
121
+ continue;
122
+ const storage = coll.storage;
123
+ if (!storage || storage.mode !== "repo")
124
+ continue;
125
+ const slug = String(coll.slug);
126
+ const records = opts.readRepoMedia?.(slug) ?? [];
127
+ for (const record of records) {
128
+ const staged = await stageRepoMedia({
129
+ collectionSlug: slug,
130
+ assetRoot,
131
+ bytes: record.bytes,
132
+ mime: record.mime,
133
+ originalFilename: record.originalFilename,
134
+ policy: storage.naming
135
+ });
136
+ if (!dryRun) {
137
+ writeFileSafe(resolve(cwd, staged.repoPath), cwd, staged.bytes);
138
+ }
139
+ media.push(staged.repoPath);
140
+ }
141
+ }
142
+ const written = [...routes, ...content, ...media];
143
+ return { written, routes, content, media };
144
+ }
145
+ function writeFileSafe(absPath, cwd, data) {
146
+ const normalized = resolve(absPath);
147
+ const cwdNorm = resolve(cwd);
148
+ if (!normalized.startsWith(cwdNorm + "/")) {
149
+ throw new Error(`compileForPublish: refusing to write outside cwd "${cwdNorm}": ${absPath}`);
150
+ }
151
+ mkdirSync(dirname(absPath), { recursive: true });
152
+ writeFileSync(absPath, data);
153
+ }
154
+ // src/tenant/index.ts
155
+ import { Effect, SchemaAST } from "effect";
156
+ function tenantScoped(opts) {
157
+ const { userField, recordField = userField } = opts;
158
+ const read = ({ user }) => {
159
+ if (user == null)
160
+ return false;
161
+ const tenantValue = user.claims[userField];
162
+ if (tenantValue == null)
163
+ return false;
164
+ return { where: { [recordField]: tenantValue } };
165
+ };
166
+ const beforeChange = (ctx) => Effect.sync(() => {
167
+ if (ctx.user == null)
168
+ return;
169
+ const tenantValue = ctx.user.claims[userField];
170
+ if (tenantValue == null)
171
+ return;
172
+ const data = ctx.data;
173
+ if (data != null) {
174
+ data[recordField] = tenantValue;
175
+ }
176
+ });
177
+ return { read, hooks: { beforeChange } };
178
+ }
179
+ function assertTenantIsolation(config, opts) {
180
+ const { tenantField, exempt = [] } = opts;
181
+ const exemptSet = new Set(exempt);
182
+ const unscoped = [];
183
+ for (const coll of config.collections ?? []) {
184
+ const slug = String(coll.slug);
185
+ if (exemptSet.has(slug))
186
+ continue;
187
+ if (!schemaHasField(coll.schema, tenantField))
188
+ continue;
189
+ if (coll.access?.read == null) {
190
+ unscoped.push(slug);
191
+ }
192
+ }
193
+ return { ok: unscoped.length === 0, unscoped };
194
+ }
195
+ function schemaHasField(schema, fieldName) {
196
+ const ast = schema.ast;
197
+ if (!SchemaAST.isTypeLiteral(ast))
198
+ return false;
199
+ return ast.propertySignatures.some((p) => String(p.name) === fieldName);
200
+ }
201
+ export {
202
+ withMediaFields,
203
+ themeFingerprint,
204
+ tenantScoped,
205
+ stageRepoMedia,
206
+ slugToTableName,
207
+ signal,
208
+ schemaToTsTypes,
209
+ schemaToTsType,
210
+ schemaToPuckFields,
211
+ schemaToOpenApiSchema,
212
+ schemaToDrizzle,
213
+ schemaToD1Migrations,
214
+ schemaToD1Migration,
215
+ runHooks,
216
+ resolveMediaRef,
217
+ resolveHooksFor,
218
+ recordAuditEvent,
219
+ normalizeDateSchemas,
220
+ mediaTransformUrl,
221
+ mediaRepoName,
222
+ id,
223
+ getServices,
224
+ fingerprintToString,
225
+ fingerprintBlockSchema,
226
+ filterOpenApiForUser,
227
+ evaluateAccess,
228
+ effect,
229
+ diffBlockSchemas,
230
+ detectPendingContentMigrations,
231
+ deriveContributedCapabilities,
232
+ defineTheme,
233
+ definePlugin,
234
+ definePageTemplate,
235
+ definePage,
236
+ defineConfig,
237
+ defineCollection,
238
+ defineBlock,
239
+ defaultLabel,
240
+ createSaacmsRuntime,
241
+ contentMigrationModuleSource,
242
+ computed,
243
+ computeScaleCostSignal,
244
+ compileForPublish,
245
+ collectionToOpenApiPaths,
246
+ collectServices,
247
+ camelToSnakeCase,
248
+ assertTenantIsolation,
249
+ assertPluginCapabilities,
250
+ assertNoTableNameCollisions,
251
+ UniqueConstraintError,
252
+ SCHEME_COOKIE,
253
+ RowStorageError,
254
+ REPO_MEDIA_FIELD_NAMES,
255
+ PLUGIN_CAPABILITIES,
256
+ MediaRef,
257
+ MediaFields,
258
+ MEDIA_FIELD_NAMES,
259
+ DEFAULT_THRESHOLDS,
260
+ DARK_MODE_COOKIE
261
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Authorization audit emit seam — ADR 0031.
3
+ *
4
+ * Provides the `AuditEvent` type, the `AuditSink` interface, and the
5
+ * `recordAuditEvent` helper. Persistence (D1 table / SIEM) is host-configured
6
+ * and OUT OF SCOPE for this module — this module is the *emit seam + interface*,
7
+ * not a storage backend.
8
+ *
9
+ * The default sink is a no-op (zero-config, never throws). The host or a plugin
10
+ * supplies a real sink by contributing `services: { auditSink: <AuditSink> }`
11
+ * in a `PluginDef` — exactly the same pattern as `services.cache` and
12
+ * `services.realtime` (ADR 0014 / `runtime/services.ts`).
13
+ *
14
+ * Per ADR 0020 §Observability, the Observability bounded context never owns
15
+ * business state; it receives domain events via a thin service interface. This
16
+ * module IS that thin interface for authorization decisions.
17
+ */
18
+ import type { Context } from "hono";
19
+ import type { User } from "../types/user.ts";
20
+ /**
21
+ * A single authorization-decision audit record.
22
+ *
23
+ * Field rationale (ADR 0027 §9 `{ op, user, collection, result, recordId }`
24
+ * extended with `decision` + `at`):
25
+ * - `op` — which CRUD verb was attempted
26
+ * - `collection` — which Collection was targeted
27
+ * - `user` — the authenticated principal (null = anonymous)
28
+ * - `decision` — the binary outcome of the access predicate
29
+ * - `reason` — human-readable context (primarily useful for `denied`)
30
+ * - `recordId` — present when the predicate evaluated at record grain
31
+ * - `at` — ISO 8601 wall-clock timestamp of the decision
32
+ */
33
+ export interface AuditEvent {
34
+ readonly op: "read" | "list" | "create" | "update" | "put" | "delete";
35
+ readonly collection: string;
36
+ readonly user: User | null;
37
+ readonly decision: "granted" | "denied";
38
+ readonly reason?: string;
39
+ readonly recordId?: string;
40
+ readonly at: string;
41
+ }
42
+ /**
43
+ * Host-supplied sink. The host or a plugin provides a concrete implementation
44
+ * via `PluginDef.services.auditSink`. Persistence (D1 table, SIEM, log
45
+ * pipeline) is entirely host-configured — this module ships no backend.
46
+ */
47
+ export interface AuditSink {
48
+ emit(e: AuditEvent): void | Promise<void>;
49
+ }
50
+ /**
51
+ * Emit an authorization audit event via the injected sink, best-effort.
52
+ *
53
+ * Best-effort contract (mirrors event-phase hooks, ADR 0013):
54
+ * - A synchronous sink throw is caught and logged; it NEVER propagates to
55
+ * the caller or alters the HTTP response.
56
+ * - An async sink rejection is caught via `.catch`; it NEVER propagates.
57
+ * - This function returns synchronously; it NEVER blocks or delays the
58
+ * request regardless of sink latency.
59
+ *
60
+ * The sink is resolved from `getServices(c).auditSink`; when no sink is
61
+ * injected (the common case), the frozen no-op sink is used — zero overhead,
62
+ * zero config required.
63
+ */
64
+ export declare function recordAuditEvent(c: Context, event: AuditEvent): void;
65
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAA;IACrE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAepE"}
@@ -0,0 +1,2 @@
1
+ export * from "./audit.ts";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Publish compile step — the "compile" half of `saacms publish`.
3
+ *
4
+ * Given a resolved `SaacmsConfig`, emits all artifacts the Publish pipeline
5
+ * commits to the developer's repo:
6
+ *
7
+ * 1. Host route files (e.g. `src/pages/<url>.astro`) for each Page in
8
+ * `config.pages`, generated via the host adapter.
9
+ * 2. Published content JSON (`content/pages/<id>.json`) for each Page —
10
+ * the page's Puck/Block tree persisted as a deterministic JSON file
11
+ * alongside the route in the commit.
12
+ * 3. Repo-mode media bytes (`<assetRoot>saacms/<slug>/<name>`) for each
13
+ * record in a `kind:"media", storage:{mode:"repo"}` Collection.
14
+ * Bucket-mode media lives in R2 and is never committed (ADR 0024).
15
+ *
16
+ * Pure w.r.t. injected readers: the CLI supplies real adapters that read from
17
+ * the live DB / R2; tests inject in-process stubs. The compile step itself
18
+ * only *writes* — it has no direct coupling to the live storage tier.
19
+ *
20
+ * Per ADR 0001 (compile step), ADR 0003 (saacms owns routing), ADR 0009
21
+ * (repo-mode media on Publish).
22
+ */
23
+ import type { SaacmsConfig } from "../schema/index.ts";
24
+ /** The manifest returned by `compileForPublish`. */
25
+ export interface CompileManifest {
26
+ /** All paths written (routes + content + media). Relative to `opts.cwd`. */
27
+ readonly written: ReadonlyArray<string>;
28
+ /** Emitted host route files (e.g. `src/pages/about.astro`). */
29
+ readonly routes: ReadonlyArray<string>;
30
+ /** Emitted published content JSON files (e.g. `content/pages/p-about.json`). */
31
+ readonly content: ReadonlyArray<string>;
32
+ /** Emitted repo-mode media files (e.g. `public/saacms/images/abc.png`). */
33
+ readonly media: ReadonlyArray<string>;
34
+ }
35
+ /** One repo-mode media record to materialize on Publish. */
36
+ export interface RepoMediaRecord {
37
+ readonly bytes: Uint8Array;
38
+ readonly mime: string;
39
+ readonly originalFilename?: string;
40
+ }
41
+ /** Options for `compileForPublish`. */
42
+ export interface CompileOptions {
43
+ /** Absolute path to the project root (files are written here). */
44
+ readonly cwd: string;
45
+ /**
46
+ * When `true`, compute the manifest but do NOT write files to disk.
47
+ * Used by `saacms publish --dry` to report what would be emitted.
48
+ * Default: `false`.
49
+ */
50
+ readonly dryRun?: boolean;
51
+ /**
52
+ * Inject the Puck/Block layout for a given page id. When omitted, the
53
+ * compile step uses `page.layout` from the config directly. Allows the CLI
54
+ * to supply the live-DB version of the layout while tests supply a stub.
55
+ */
56
+ readonly readPageContent?: (pageId: string) => unknown;
57
+ /**
58
+ * Inject repo-mode media records for a given collection slug. Each record
59
+ * carries the raw bytes, MIME type, and optional original filename. When
60
+ * omitted, no media is written (the collection has no pending uploads).
61
+ */
62
+ readonly readRepoMedia?: (collectionSlug: string) => ReadonlyArray<RepoMediaRecord>;
63
+ }
64
+ /**
65
+ * Emit all publish artifacts for the given `SaacmsConfig` into `opts.cwd`.
66
+ *
67
+ * Files are written deterministically: same config + same content → same
68
+ * bytes at the same paths. The compile step never writes outside `opts.cwd`
69
+ * (path-safety enforced by `writeFileSafe`).
70
+ *
71
+ * Returns a manifest of all written (or, in dryRun mode, would-be-written)
72
+ * paths relative to `opts.cwd`. The CLI passes these exact paths to
73
+ * `git add` so only saacms-emitted artifacts are staged.
74
+ */
75
+ export declare function compileForPublish(config: SaacmsConfig, opts: CompileOptions): Promise<CompileManifest>;
76
+ //# sourceMappingURL=compile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/publish/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AA+BtD,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACtC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACtC;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACtD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,cAAc,EAAE,MAAM,KACnB,aAAa,CAAC,eAAe,CAAC,CAAA;CACpC;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,eAAe,CAAC,CA6E1B"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Auth middleware — resolves the calling `User` once per request and stashes
3
+ * it under `c.set("user", ...)` so downstream handlers can read it.
4
+ *
5
+ * Per ADR 0008 + ADR 0020 (Identity bounded context), the runtime never imports
6
+ * a concrete auth library; the host wires a `config.auth: AuthAdapter`, and
7
+ * this middleware bridges it into the Hono request lifecycle.
8
+ *
9
+ * Mounted FIRST in `runtime/index.ts` so all downstream route handlers see a
10
+ * resolved user (or `undefined` for anonymous) when they read `c.get("user")`.
11
+ *
12
+ * Contract:
13
+ * - `config.auth` undefined → no middleware is registered at all
14
+ * (anonymous-only mode; the runtime stays usable before the host wires auth).
15
+ * - Otherwise, for every request:
16
+ * 1. call `config.auth.getSession(c.req.raw)` (the underlying Web Request),
17
+ * 2. on non-null `User` → `c.set("user", user)`,
18
+ * 3. on `null` → leave the variable unset,
19
+ * 4. on thrown exception → log via `console.warn` with the `[saacms/auth]`
20
+ * prefix and leave the variable unset.
21
+ * - The middleware NEVER rejects a request. Auth-required routes emit 401/403
22
+ * themselves via the `access` predicate.
23
+ */
24
+ import type { Context, Env, Hono } from "hono";
25
+ import type { SaacmsConfig } from "../schema/index.ts";
26
+ import type { User } from "../types/user.ts";
27
+ export declare function mountAuthMiddleware<E extends Env>(app: Hono<E>, config: SaacmsConfig): void;
28
+ /**
29
+ * Read the user that the middleware stashed onto the context, or `null` for
30
+ * anonymous. The route handlers currently inline this cast; this helper exists
31
+ * so future refactors can converge on a single read site.
32
+ */
33
+ export declare function getUser(c: Context): User | null;
34
+ //# sourceMappingURL=auth-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../../src/runtime/auth-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAI5C,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,GAAG,EAC/C,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EACZ,MAAM,EAAE,YAAY,GACnB,IAAI,CAuBN;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAG/C"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Schema-driven Boolean ↔ INTEGER (de)serialisation at the runtime storage
3
+ * boundary.
4
+ *
5
+ * Per ADR 0005 the Effect Schema is the single source of truth; per ADR 0020
6
+ * the storage adapter is a schema-AGNOSTIC anti-corruption layer that only
7
+ * knows snake↔camel and never the column types. SQLite has no boolean type, so
8
+ * `to-d1-migration.ts` maps `Schema.Boolean` to an `INTEGER` column. The
9
+ * projection of a `boolean` domain field to its `0`/`1` integer cell (and
10
+ * back) therefore cannot live in the adapter — it lives here, at the runtime
11
+ * boundary, where the collection's Effect Schema is already in hand. This is
12
+ * the exact sibling of `json-columns.ts` for the scalar boolean case.
13
+ *
14
+ * - `encodeBooleanColumns` — map `true→1` / `false→0` for the boolean-typed
15
+ * fields of a record immediately BEFORE `rows.insert/update`.
16
+ * - `decodeBooleanColumns` — map the stored `1`/`0` (SQLite may surface it as
17
+ * a number OR a bigint) back to `true`/`false` immediately AFTER
18
+ * `rows.getById/list`.
19
+ *
20
+ * Without `decodeBooleanColumns` a `Schema.Boolean` create persists `1`/`0`
21
+ * but reads back as a *number*, so a subsequent PATCH (which re-validates the
22
+ * merged record against the Effect Schema) sees `1` where `boolean` is
23
+ * expected → a validation error. This is the identical failure shape the JSON
24
+ * gap had.
25
+ *
26
+ * Contract (identical to `json-columns.ts`):
27
+ * - Which fields are "boolean" is derived from the SAME AST the codegen walks
28
+ * (post optional/nullable/refinement stripping) via `AST.isBooleanKeyword`
29
+ * — the exact test `to-d1-migration.ts` uses to choose `INTEGER`, so codec
30
+ * and DDL can never disagree. Non-boolean primitives and the complex
31
+ * shapes `json-columns.ts` owns are never touched (disjoint by AST kind).
32
+ * - `null` / absent values are left untouched, composing correctly with the
33
+ * adapter's NULL→absent read behaviour (ADR 0020) — an absent optional
34
+ * boolean never becomes `0`/`false`.
35
+ * - Symmetric + idempotent for valid data: `decode∘encode = identity`;
36
+ * `decode` of an already-decoded (boolean) value is a no-op, so it is safe
37
+ * to run on a cache-hit raw record or a freshly-decoded one.
38
+ * - Pure: never mutates its input record; returns a shallow copy.
39
+ *
40
+ * Composition with `json-columns.ts`: the two codecs operate on disjoint field
41
+ * sets (boolean keyword vs. tuple/struct/unknown/any/object), so on the write
42
+ * path the order of `encodeBooleanColumns`/`encodeJsonColumns` is irrelevant,
43
+ * and likewise for the decode pair on the read path. The disjointness is
44
+ * asserted by a test.
45
+ */
46
+ import type { AnySchema } from "../schema/index.ts";
47
+ /**
48
+ * Return a shallow copy of `record` with every boolean-typed field mapped
49
+ * `true→1` / `false→0`, ready for `rows.insert/update`. `null`/absent fields
50
+ * are passed through unchanged so the adapter's NULL handling still applies.
51
+ * A non-boolean value (e.g. an already-encoded `0`/`1`) is left as-is, which
52
+ * keeps `encode` idempotent.
53
+ */
54
+ export declare function encodeBooleanColumns<T extends Record<string, unknown>>(schema: AnySchema, record: T): T;
55
+ /**
56
+ * Return a shallow copy of `record` with every boolean-typed field mapped from
57
+ * its stored integer back to a real `boolean`. SQLite may surface the cell as
58
+ * a number OR a bigint, so `1`/`1n`→`true` and `0`/`0n`→`false`. Values that
59
+ * are already boolean (already decoded) or `null`/absent are left untouched,
60
+ * so this is idempotent and safe to run on a raw storage row OR an
61
+ * already-decoded one. Any other value is left unchanged (defensive — never
62
+ * throw on a read path).
63
+ */
64
+ export declare function decodeBooleanColumns<T extends Record<string, unknown>>(schema: AnySchema, record: T): T;
65
+ //# sourceMappingURL=boolean-columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boolean-columns.d.ts","sourceRoot":"","sources":["../../src/runtime/boolean-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAmDnD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,CAAC,GACR,CAAC,CAWH;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,CAAC,GACR,CAAC,CAiBH"}