@obh/forge 0.2.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/capabilities/analytics.d.ts +3 -0
  4. package/dist/capabilities/analytics.d.ts.map +1 -0
  5. package/dist/capabilities/analytics.js +91 -0
  6. package/dist/capabilities/analytics.js.map +1 -0
  7. package/dist/capabilities/api-keys.d.ts +3 -0
  8. package/dist/capabilities/api-keys.d.ts.map +1 -0
  9. package/dist/capabilities/api-keys.js +90 -0
  10. package/dist/capabilities/api-keys.js.map +1 -0
  11. package/dist/capabilities/audit.d.ts +3 -0
  12. package/dist/capabilities/audit.d.ts.map +1 -0
  13. package/dist/capabilities/audit.js +74 -0
  14. package/dist/capabilities/audit.js.map +1 -0
  15. package/dist/capabilities/entitlements.d.ts +3 -0
  16. package/dist/capabilities/entitlements.d.ts.map +1 -0
  17. package/dist/capabilities/entitlements.js +94 -0
  18. package/dist/capabilities/entitlements.js.map +1 -0
  19. package/dist/capabilities/events.d.ts +3 -0
  20. package/dist/capabilities/events.d.ts.map +1 -0
  21. package/dist/capabilities/events.js +107 -0
  22. package/dist/capabilities/events.js.map +1 -0
  23. package/dist/capabilities/files.d.ts +3 -0
  24. package/dist/capabilities/files.d.ts.map +1 -0
  25. package/dist/capabilities/files.js +100 -0
  26. package/dist/capabilities/files.js.map +1 -0
  27. package/dist/capabilities/helpers.d.ts +27 -0
  28. package/dist/capabilities/helpers.d.ts.map +1 -0
  29. package/dist/capabilities/helpers.js +63 -0
  30. package/dist/capabilities/helpers.js.map +1 -0
  31. package/dist/capabilities/import-export.d.ts +3 -0
  32. package/dist/capabilities/import-export.d.ts.map +1 -0
  33. package/dist/capabilities/import-export.js +267 -0
  34. package/dist/capabilities/import-export.js.map +1 -0
  35. package/dist/capabilities/index.d.ts +16 -0
  36. package/dist/capabilities/index.d.ts.map +1 -0
  37. package/dist/capabilities/index.js +71 -0
  38. package/dist/capabilities/index.js.map +1 -0
  39. package/dist/capabilities/jobs.d.ts +3 -0
  40. package/dist/capabilities/jobs.d.ts.map +1 -0
  41. package/dist/capabilities/jobs.js +117 -0
  42. package/dist/capabilities/jobs.js.map +1 -0
  43. package/dist/capabilities/notifications.d.ts +3 -0
  44. package/dist/capabilities/notifications.d.ts.map +1 -0
  45. package/dist/capabilities/notifications.js +113 -0
  46. package/dist/capabilities/notifications.js.map +1 -0
  47. package/dist/capabilities/search.d.ts +3 -0
  48. package/dist/capabilities/search.d.ts.map +1 -0
  49. package/dist/capabilities/search.js +121 -0
  50. package/dist/capabilities/search.js.map +1 -0
  51. package/dist/capabilities/settings.d.ts +3 -0
  52. package/dist/capabilities/settings.d.ts.map +1 -0
  53. package/dist/capabilities/settings.js +75 -0
  54. package/dist/capabilities/settings.js.map +1 -0
  55. package/dist/capabilities/types.d.ts +16 -0
  56. package/dist/capabilities/types.d.ts.map +1 -0
  57. package/dist/capabilities/types.js +3 -0
  58. package/dist/capabilities/types.js.map +1 -0
  59. package/dist/capabilities/webhooks.d.ts +3 -0
  60. package/dist/capabilities/webhooks.d.ts.map +1 -0
  61. package/dist/capabilities/webhooks.js +117 -0
  62. package/dist/capabilities/webhooks.js.map +1 -0
  63. package/dist/cli.d.ts +3 -0
  64. package/dist/cli.d.ts.map +1 -0
  65. package/dist/cli.js +60 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/commands/add.d.ts +3 -0
  68. package/dist/commands/add.d.ts.map +1 -0
  69. package/dist/commands/add.js +88 -0
  70. package/dist/commands/add.js.map +1 -0
  71. package/dist/commands/doctor.d.ts +3 -0
  72. package/dist/commands/doctor.d.ts.map +1 -0
  73. package/dist/commands/doctor.js +94 -0
  74. package/dist/commands/doctor.js.map +1 -0
  75. package/dist/commands/generate.d.ts +3 -0
  76. package/dist/commands/generate.d.ts.map +1 -0
  77. package/dist/commands/generate.js +41 -0
  78. package/dist/commands/generate.js.map +1 -0
  79. package/dist/commands/inspect.d.ts +3 -0
  80. package/dist/commands/inspect.d.ts.map +1 -0
  81. package/dist/commands/inspect.js +64 -0
  82. package/dist/commands/inspect.js.map +1 -0
  83. package/dist/commands/new.d.ts +18 -0
  84. package/dist/commands/new.d.ts.map +1 -0
  85. package/dist/commands/new.js +149 -0
  86. package/dist/commands/new.js.map +1 -0
  87. package/dist/commands/shared.d.ts +14 -0
  88. package/dist/commands/shared.d.ts.map +1 -0
  89. package/dist/commands/shared.js +36 -0
  90. package/dist/commands/shared.js.map +1 -0
  91. package/dist/commands/skill.d.ts +3 -0
  92. package/dist/commands/skill.d.ts.map +1 -0
  93. package/dist/commands/skill.js +59 -0
  94. package/dist/commands/skill.js.map +1 -0
  95. package/dist/commands/stack.d.ts +13 -0
  96. package/dist/commands/stack.d.ts.map +1 -0
  97. package/dist/commands/stack.js +192 -0
  98. package/dist/commands/stack.js.map +1 -0
  99. package/dist/generators/api.d.ts +16 -0
  100. package/dist/generators/api.d.ts.map +1 -0
  101. package/dist/generators/api.js +375 -0
  102. package/dist/generators/api.js.map +1 -0
  103. package/dist/generators/lwd.d.ts +12 -0
  104. package/dist/generators/lwd.d.ts.map +1 -0
  105. package/dist/generators/lwd.js +143 -0
  106. package/dist/generators/lwd.js.map +1 -0
  107. package/dist/generators/mobile.d.ts +16 -0
  108. package/dist/generators/mobile.d.ts.map +1 -0
  109. package/dist/generators/mobile.js +180 -0
  110. package/dist/generators/mobile.js.map +1 -0
  111. package/dist/generators/platform-package.d.ts +18 -0
  112. package/dist/generators/platform-package.d.ts.map +1 -0
  113. package/dist/generators/platform-package.js +568 -0
  114. package/dist/generators/platform-package.js.map +1 -0
  115. package/dist/generators/root.d.ts +15 -0
  116. package/dist/generators/root.d.ts.map +1 -0
  117. package/dist/generators/root.js +249 -0
  118. package/dist/generators/root.js.map +1 -0
  119. package/dist/generators/sdk.d.ts +14 -0
  120. package/dist/generators/sdk.d.ts.map +1 -0
  121. package/dist/generators/sdk.js +137 -0
  122. package/dist/generators/sdk.js.map +1 -0
  123. package/dist/generators/web.d.ts +15 -0
  124. package/dist/generators/web.d.ts.map +1 -0
  125. package/dist/generators/web.js +203 -0
  126. package/dist/generators/web.js.map +1 -0
  127. package/dist/generators/worker.d.ts +13 -0
  128. package/dist/generators/worker.d.ts.map +1 -0
  129. package/dist/generators/worker.js +148 -0
  130. package/dist/generators/worker.js.map +1 -0
  131. package/dist/index.d.ts +10 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +44 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/project/context.d.ts +24 -0
  136. package/dist/project/context.d.ts.map +1 -0
  137. package/dist/project/context.js +63 -0
  138. package/dist/project/context.js.map +1 -0
  139. package/dist/project/manifest.d.ts +26 -0
  140. package/dist/project/manifest.d.ts.map +1 -0
  141. package/dist/project/manifest.js +73 -0
  142. package/dist/project/manifest.js.map +1 -0
  143. package/dist/project/paths.d.ts +25 -0
  144. package/dist/project/paths.d.ts.map +1 -0
  145. package/dist/project/paths.js +51 -0
  146. package/dist/project/paths.js.map +1 -0
  147. package/dist/project/plan.d.ts +42 -0
  148. package/dist/project/plan.d.ts.map +1 -0
  149. package/dist/project/plan.js +251 -0
  150. package/dist/project/plan.js.map +1 -0
  151. package/dist/prompts/index.d.ts +24 -0
  152. package/dist/prompts/index.d.ts.map +1 -0
  153. package/dist/prompts/index.js +111 -0
  154. package/dist/prompts/index.js.map +1 -0
  155. package/dist/recipes/index.d.ts +25 -0
  156. package/dist/recipes/index.d.ts.map +1 -0
  157. package/dist/recipes/index.js +77 -0
  158. package/dist/recipes/index.js.map +1 -0
  159. package/dist/stack/infer.d.ts +4 -0
  160. package/dist/stack/infer.d.ts.map +1 -0
  161. package/dist/stack/infer.js +165 -0
  162. package/dist/stack/infer.js.map +1 -0
  163. package/dist/stack/lwd.d.ts +24 -0
  164. package/dist/stack/lwd.d.ts.map +1 -0
  165. package/dist/stack/lwd.js +62 -0
  166. package/dist/stack/lwd.js.map +1 -0
  167. package/dist/stack/lwdtoml.d.ts +38 -0
  168. package/dist/stack/lwdtoml.d.ts.map +1 -0
  169. package/dist/stack/lwdtoml.js +57 -0
  170. package/dist/stack/lwdtoml.js.map +1 -0
  171. package/dist/stack/manifest.d.ts +15 -0
  172. package/dist/stack/manifest.d.ts.map +1 -0
  173. package/dist/stack/manifest.js +86 -0
  174. package/dist/stack/manifest.js.map +1 -0
  175. package/dist/stack/types.d.ts +48 -0
  176. package/dist/stack/types.d.ts.map +1 -0
  177. package/dist/stack/types.js +5 -0
  178. package/dist/stack/types.js.map +1 -0
  179. package/dist/stack/wire.d.ts +40 -0
  180. package/dist/stack/wire.d.ts.map +1 -0
  181. package/dist/stack/wire.js +192 -0
  182. package/dist/stack/wire.js.map +1 -0
  183. package/dist/types.d.ts +114 -0
  184. package/dist/types.d.ts.map +1 -0
  185. package/dist/types.js +5 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/utils/fs.d.ts +13 -0
  188. package/dist/utils/fs.d.ts.map +1 -0
  189. package/dist/utils/fs.js +64 -0
  190. package/dist/utils/fs.js.map +1 -0
  191. package/dist/utils/git.d.ts +17 -0
  192. package/dist/utils/git.d.ts.map +1 -0
  193. package/dist/utils/git.js +49 -0
  194. package/dist/utils/git.js.map +1 -0
  195. package/dist/utils/logger.d.ts +16 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +21 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/shell.d.ts +21 -0
  200. package/dist/utils/shell.d.ts.map +1 -0
  201. package/dist/utils/shell.js +40 -0
  202. package/dist/utils/shell.js.map +1 -0
  203. package/dist/version.d.ts +2 -0
  204. package/dist/version.d.ts.map +1 -0
  205. package/dist/version.js +6 -0
  206. package/dist/version.js.map +1 -0
  207. package/package.json +40 -0
  208. package/skills/obh-add-events/SKILL.md +26 -0
  209. package/skills/obh-generate-audit-rules/SKILL.md +26 -0
  210. package/skills/obh-inspect-project/SKILL.md +26 -0
  211. package/skills/obh-lwd-manifest/SKILL.md +26 -0
  212. package/skills/obh-retrofit-files/SKILL.md +24 -0
  213. package/skills/obh-retrofit-jobs/SKILL.md +26 -0
  214. package/skills/obh-sdk-extraction/SKILL.md +24 -0
  215. package/skills/obh-settings-migration/SKILL.md +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OBH Software
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # OBH Forge
2
+
3
+ Deterministic scaffolding and delivery tooling for OBH software projects.
4
+
5
+ Forge starts projects correctly, then gets out of the way. It is **not** a
6
+ runtime, framework, or reconciler — just a set of composable CLI commands that
7
+ generate boring, readable, removable code.
8
+
9
+ ```bash
10
+ forge new app qhaul --recipe full-saas # api + admin + worker + sdk + core primitives
11
+ forge add search # add a primitive (prerequisites auto-added)
12
+ forge add notifications
13
+ forge generate lwd # (re)generate deploy manifests
14
+ forge inspect # what's in this project?
15
+ forge doctor # does it match OBH conventions?
16
+ forge skill install obh-add-events # drop a Claude skill into .claude/skills/
17
+ ```
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pnpm add -g @obh/forge # once published
23
+ # or from source:
24
+ pnpm install && pnpm build && node dist/cli.js --help
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ | Command | What it does |
30
+ |---|---|
31
+ | `forge new app <name>` | Scaffold a new project. Recipes: `full-saas`, `api-web-worker`, `api-only`, `worker`. Flags: `--api-framework hono\|express`, `--no-example`, `--no-sdk`, `--mobile [name]`, `--topology small\|split`, `--repo owner/repo`, `--scope`. |
32
+ | `forge new package <name>` | Scaffold an OBH platform-primitive repo (the qtool template: `@obh/<name>`, kernel helpers, record-store client, migrations, CI). `--daemon` adds an admin/worker app. |
33
+ | `forge add <target>` | Add an app (`api`/`web`/`worker`/`sdk`/`mobile`) or a primitive — `events`, `jobs`, `files`, `audit`, `settings`, `api-keys`, `webhooks`, `import-export`, `entitlements`, `search`, `analytics`, `notifications`. Prerequisites resolve automatically. |
34
+ | `forge inspect` | Report project shape and detected conventions. |
35
+ | `forge doctor` | Check the project against OBH conventions (reports only). |
36
+ | `forge generate <artifact>` | Regenerate `lwd`, `ci`, or `env`. |
37
+ | `forge skill <list\|install>` | List or install the bundled OBH Claude skills. |
38
+ | `forge stack <init\|deploy\|status\|rm>` | Deliver the repo to lwd: assemble the apps into one stack, generate + wire secret values, and apply in order. `forge deploy` is an alias for `forge stack deploy`. |
39
+
40
+ Every mutating command supports `--dry-run`, `--yes`, and `--force`.
41
+
42
+ ## Deploying a stack (`forge stack`)
43
+
44
+ `forge new` generates per-app `deploy/*.lwd.toml` with **secret names only**.
45
+ `forge stack` closes the gap to a running deployment — as a pure client of the
46
+ `lwd` CLI (zero lwd changes):
47
+
48
+ ```bash
49
+ forge stack init # inspect deploy/*.lwd.toml → write a reviewable deploy/stack.json
50
+ # (groups the apps, proposes generated + derived secrets)
51
+ # review deploy/stack.json
52
+ forge stack deploy # (alias: forge deploy) generate secret VALUES, wire connections,
53
+ # lwd secret set + lwd apply each app in dependency order
54
+ forge stack status # rolled-up lwd status across the stack
55
+ forge stack rm # tear down in reverse order (named data volumes preserved)
56
+ ```
57
+
58
+ `deploy` **generates** the random secrets (`POSTGRES_PASSWORD`, `JWT_SECRET`,
59
+ `API_KEYS_PEPPER`), **derives** connection strings (`DATABASE_URL` from the
60
+ Postgres backing service), **orders** the multi-app apply, and is **idempotent**
61
+ (lwd is the sole secret store — already-set secrets are skipped, never rotated
62
+ silently; `--dry-run` masks all values). It **honestly flags** lwd's per-app
63
+ network isolation: a worker that can't reach a Postgres co-located in the API is
64
+ reported (fix by moving to split topology), never wired with a broken value.
65
+ `--rotate <KEY>` regenerates a secret and re-derives everything built from it.
66
+
67
+ ## What you get from `forge new`
68
+
69
+ A real, working vertical slice — not stubs:
70
+
71
+ - **`apps/api`** — Hono or Express, a Postgres pool with a `withTx` helper, a
72
+ transaction-aware **event bus**, and (unless `--no-example`) a real `notes`
73
+ domain: CRUD over Postgres that emits `note.created/updated/deleted` facts
74
+ inside its write transaction. Routes auto-mount from `src/routes/*`.
75
+ - **`apps/admin`** — Vite + React, a real Notes management UI driving the API
76
+ through the shared SDK.
77
+ - **`apps/worker`** — a tick loop with a health port that auto-loads consumers
78
+ from `src/consumers.d/*`.
79
+ - **`packages/sdk`** — the one typed contract (types + client) shared by the API
80
+ and every frontend.
81
+ - **`apps/mobile`** (with `--mobile`) — an Expo React Native app driving the same
82
+ SDK; the notes screen works end-to-end. Ships via EAS, so it gets no lwd
83
+ manifest.
84
+ - **`deploy/*.lwd.toml`**, per-app **Dockerfiles**, **CI** (with Postgres),
85
+ a **migration runner**, `.env.example`, `forge.json`.
86
+
87
+ ## How capabilities stay safe
88
+
89
+ Adding a primitive never patches your code. Generated apps auto-load from
90
+ convention directories, so `forge add <x>` just drops files:
91
+
92
+ - API routes → `apps/api/src/routes/*` (each `register(app)`)
93
+ - domain-fact subscribers → `apps/api/src/bus.d/*` (`onEmit(...)`)
94
+ - worker consumers → `apps/worker/src/consumers.d/*` (`init`/`tick`)
95
+ - platform migrations → `scripts/migrations.d/*` (`migrate(pool)`)
96
+
97
+ Every change flows through a `Plan` that classifies each op as create / append /
98
+ overwrite and refuses to clobber a differing file without `--force`.
99
+
100
+ Primitives wire into the example domain for real: `events` bridges the bus to the
101
+ durable outbox, `jobs` enqueues on note writes, `audit` records `note.*`,
102
+ `search`/`analytics` index and roll up note events, `notifications` emails on
103
+ `note.created`, `import-export` moves notes as CSV, `entitlements` gates note
104
+ creation, `api-keys` protects a machine route.
105
+
106
+ ## Design guarantees
107
+
108
+ - **Boring, real output.** Generated projects are normal TypeScript you can read,
109
+ edit, or delete. No TODOs, no empty handlers, no placeholder URLs (deploy repos
110
+ are auto-detected from `git remote`, else prompted).
111
+ - **Composable & opt-in.** Nothing is installed unless a recipe or `forge add`
112
+ asks for it.
113
+ - **Configurable, not rigid.** Framework choice, example on/off, SDK on/off,
114
+ topology, custom app names — all recorded in `forge.json`.
115
+ - **lwd-aware.** Manifests encode lwd's real constraints (workers are separate
116
+ surfaces; a scalable API can't co-locate a backing DB; per-app network
117
+ isolation). Secret names only — never values.
118
+
119
+ ## forge.json
120
+
121
+ Each project carries a `forge.json` recording shape, chosen conventions, and
122
+ installed primitives. It's a receipt and an aid for `forge doctor` and the Claude
123
+ skills — not a desired-state contract.
124
+
125
+ ## Claude skills
126
+
127
+ Bundled under `skills/` and installable per-project with `forge skill install`:
128
+ `obh-inspect-project`, `obh-add-events`, `obh-retrofit-jobs`, `obh-retrofit-files`,
129
+ `obh-generate-audit-rules`, `obh-settings-migration`, `obh-lwd-manifest`,
130
+ `obh-sdk-extraction`. Forge does the mechanical file work; the skills do the
131
+ interpretive work (reading a repo, proposing event names, mapping legacy code).
132
+
133
+ ## Deferred
134
+
135
+ AST-level patching and an upgrade/reconcile mode.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ pnpm install
141
+ pnpm build
142
+ pnpm typecheck
143
+ pnpm test
144
+ ```
145
+
146
+ Requires Node ≥ 20 and pnpm 9.
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const analytics: Capability;
3
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/capabilities/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,SAAS,EAAE,UA8BvB,CAAA"}
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analytics = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.analytics = {
6
+ name: "analytics",
7
+ requires: ["events"],
8
+ describe: "Event-derived KPIs — facts rolled into queryable timeseries",
9
+ apply(ctx, plan) {
10
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "analytics", { api: true, worker: true });
11
+ plan.create("scripts/migrations.d/analytics.ts", (0, helpers_1.migrationModule)("analytics"), "analytics migrations wiring");
12
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
13
+ if (ctx.hasApp("api")) {
14
+ plan.create("apps/api/src/platform/analytics.ts", apiClient(notes), "analytics client + metrics");
15
+ plan.create("apps/api/src/routes/analytics.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "metric timeseries route");
16
+ }
17
+ if (ctx.hasWorker()) {
18
+ plan.create("apps/worker/src/analytics-metrics.ts", workerMetrics(notes), "worker-side metric definitions");
19
+ plan.create("apps/worker/src/dispatch.d/analytics.ts", `export const consumer = { name: "analytics", events: ["*"] }\n`, "register analytics as an event consumer");
20
+ plan.create("apps/worker/src/consumers.d/analytics.ts", WORKER, "analytics rollup tick");
21
+ }
22
+ plan.patchManifest({ platform: { analytics: true } });
23
+ plan.nextStep("Run `pnpm migrate`. Query metrics at GET /api/analytics/:metric.");
24
+ },
25
+ };
26
+ function metricDefs(notes) {
27
+ return notes
28
+ ? ` defineMetric({ key: "notes_created", events: "note.created", aggregation: "count" }),`
29
+ : ` // defineMetric({ key: "things_created", events: "thing.created", aggregation: "count" }),`;
30
+ }
31
+ function apiClient(notes) {
32
+ return `import { createAnalyticsClient, defineMetric, pgAdapter, type MetricDefinition } from "@obh/analytics"
33
+ import { pool } from "../db"
34
+
35
+ export const metrics: MetricDefinition[] = [
36
+ ${metricDefs(notes)}
37
+ ]
38
+
39
+ export const analytics = createAnalyticsClient({ db: pgAdapter(pool), metrics })
40
+ `;
41
+ }
42
+ function workerMetrics(notes) {
43
+ return `import { defineMetric, type MetricDefinition } from "@obh/analytics"
44
+
45
+ // Worker-side copy of the metric definitions (kept here so the worker never
46
+ // imports across app boundaries). Mirror of apps/api/src/platform/analytics.ts.
47
+ export const metrics: MetricDefinition[] = [
48
+ ${metricDefs(notes)}
49
+ ]
50
+ `;
51
+ }
52
+ const ROUTE_HONO = `import type { Hono } from "hono"
53
+ import type { Bucket } from "@obh/analytics"
54
+ import { analytics } from "../platform/analytics"
55
+ import { WORKSPACE } from "../platform/events"
56
+
57
+ export function register(app: Hono): void {
58
+ app.get("/api/analytics/:metric", async (c) => {
59
+ const bucket = (c.req.query("bucket") as Bucket) ?? "day"
60
+ return c.json(await analytics.timeseries({ workspaceId: WORKSPACE, metric: c.req.param("metric"), bucket }))
61
+ })
62
+ }
63
+ `;
64
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
65
+ import type { Bucket } from "@obh/analytics"
66
+ import { analytics } from "../platform/analytics"
67
+ import { WORKSPACE } from "../platform/events"
68
+
69
+ export function register(app: Express): void {
70
+ app.get("/api/analytics/:metric", async (req: Request, res: Response) => {
71
+ const bucket = (req.query.bucket as Bucket) ?? "day"
72
+ res.json(await analytics.timeseries({ workspaceId: WORKSPACE, metric: req.params.metric, bucket }))
73
+ })
74
+ }
75
+ `;
76
+ const WORKER = `// Claims the "analytics" deliveries and rolls facts into metric buckets.
77
+ import { createAnalyticsWorker, pgAdapter } from "@obh/analytics"
78
+ import { metrics } from "../analytics-metrics"
79
+ import type { WorkerContext } from "../context"
80
+
81
+ let worker: ReturnType<typeof createAnalyticsWorker>
82
+
83
+ export function init(ctx: WorkerContext): void {
84
+ worker = createAnalyticsWorker({ db: pgAdapter(ctx.pool), metrics, instanceId: "worker" })
85
+ }
86
+
87
+ export async function tick(): Promise<void> {
88
+ await worker.tick()
89
+ }
90
+ `;
91
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/capabilities/analytics.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,SAAS,GAAe;IACnC,IAAI,EAAE,WAAW;IACjB,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,6DAA6D;IACvE,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,mCAAmC,EAAE,IAAA,yBAAe,EAAC,WAAW,CAAC,EAAE,6BAA6B,CAAC,CAAA;QAE7G,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,oCAAoC,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,4BAA4B,CAAC,CAAA;YACjG,IAAI,CAAC,MAAM,CACT,kCAAkC,EAClC,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EACzD,yBAAyB,CAC1B,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,gCAAgC,CAAC,CAAA;YAC3G,IAAI,CAAC,MAAM,CACT,yCAAyC,EACzC,gEAAgE,EAChE,yCAAyC,CAC1C,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,0CAA0C,EAAE,MAAM,EAAE,uBAAuB,CAAC,CAAA;QAC1F,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACrD,IAAI,CAAC,QAAQ,CAAC,kEAAkE,CAAC,CAAA;IACnF,CAAC;CACF,CAAA;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,KAAK;QACV,CAAC,CAAC,yFAAyF;QAC3F,CAAC,CAAC,8FAA8F,CAAA;AACpG,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO;;;;EAIP,UAAU,CAAC,KAAK,CAAC;;;;CAIlB,CAAA;AACD,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO;;;;;EAKP,UAAU,CAAC,KAAK,CAAC;;CAElB,CAAA;AACD,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;;CAWlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;;CAWrB,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;CAcd,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const apiKeys: Capability;
3
+ //# sourceMappingURL=api-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../../src/capabilities/api-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,OAAO,EAAE,UAwBrB,CAAA"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiKeys = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.apiKeys = {
6
+ name: "api-keys",
7
+ describe: "Scoped machine-to-machine API keys (non-human credentials)",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "api-keys", { api: true });
10
+ plan.create("scripts/migrations.d/api-keys.ts", (0, helpers_1.migrationModule)("api-keys"), "api-keys migrations wiring");
11
+ if (ctx.hasApp("api")) {
12
+ plan.create("apps/api/src/platform/api-keys.ts", CLIENT, "api-keys client");
13
+ const hono = (0, helpers_1.apiFramework)(ctx) === "hono";
14
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
15
+ plan.create("apps/api/src/routes/machine.ts", hono ? routeHono(notes) : routeExpress(notes), "scope-protected machine route");
16
+ }
17
+ plan.addEnvVar({
18
+ name: "API_KEYS_PEPPER",
19
+ example: "change-me-to-a-long-random-secret",
20
+ comment: "REQUIRED — api-keys won't work without it; keep out of the DB",
21
+ secret: true,
22
+ });
23
+ plan.patchManifest({ platform: { "api-keys": true } });
24
+ plan.nextStep("Run `pnpm migrate` and set API_KEYS_PEPPER. Issue keys via apiKeys.create({ workspaceId, name, scopes }).");
25
+ },
26
+ };
27
+ const CLIENT = `import { createApiKeysClient, pgAdapter } from "@obh/api-keys"
28
+ import { pool } from "../db"
29
+
30
+ export const apiKeys = createApiKeysClient({
31
+ db: pgAdapter(pool),
32
+ pepper: process.env.API_KEYS_PEPPER!,
33
+ })
34
+ `;
35
+ function routeHono(notes) {
36
+ const body = notes
37
+ ? ` const body = await c.req.json<{ title?: string; body?: string }>()
38
+ if (!body.title) return c.json({ error: "title is required" }, 400)
39
+ return c.json(await notes.createNote({ title: body.title, body: body.body }), 201)`
40
+ : ` return c.json({ ok: true, principal: ctx.principalId })`;
41
+ return `import type { Hono } from "hono"
42
+ import { ApiKeyAuthError } from "@obh/api-keys"
43
+ import { apiKeys } from "../platform/api-keys"
44
+ ${notes ? 'import * as notes from "../domain/notes"\n' : ""}
45
+ export function register(app: Hono): void {
46
+ app.post("/machine/${notes ? "notes" : "ping"}", async (c) => {
47
+ const bearer = (c.req.header("authorization") ?? "").replace(/^Bearer /, "")
48
+ let ctx
49
+ try {
50
+ ctx = await apiKeys.authenticate(bearer)
51
+ } catch (err) {
52
+ if (err instanceof ApiKeyAuthError) return c.json({ error: "invalid api key" }, 401)
53
+ throw err
54
+ }
55
+ if (!apiKeys.hasScope(ctx, "${notes ? "notes:write" : "machine:use"}")) {
56
+ return c.json({ error: "missing scope" }, 403)
57
+ }
58
+ ${body}
59
+ })
60
+ }
61
+ `;
62
+ }
63
+ function routeExpress(notes) {
64
+ const body = notes
65
+ ? ` if (!req.body.title) return res.status(400).json({ error: "title is required" })
66
+ res.status(201).json(await notes.createNote({ title: req.body.title, body: req.body.body }))`
67
+ : ` res.json({ ok: true, principal: ctx.principalId })`;
68
+ return `import type { Express, Request, Response } from "express"
69
+ import { ApiKeyAuthError } from "@obh/api-keys"
70
+ import { apiKeys } from "../platform/api-keys"
71
+ ${notes ? 'import * as notes from "../domain/notes"\n' : ""}
72
+ export function register(app: Express): void {
73
+ app.post("/machine/${notes ? "notes" : "ping"}", async (req: Request, res: Response) => {
74
+ const bearer = (req.headers.authorization ?? "").replace(/^Bearer /, "")
75
+ let ctx
76
+ try {
77
+ ctx = await apiKeys.authenticate(bearer)
78
+ } catch (err) {
79
+ if (err instanceof ApiKeyAuthError) return res.status(401).json({ error: "invalid api key" })
80
+ throw err
81
+ }
82
+ if (!apiKeys.hasScope(ctx, "${notes ? "notes:write" : "machine:use"}")) {
83
+ return res.status(403).json({ error: "missing scope" })
84
+ }
85
+ ${body}
86
+ })
87
+ }
88
+ `;
89
+ }
90
+ //# sourceMappingURL=api-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../../src/capabilities/api-keys.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,OAAO,GAAe;IACjC,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,4DAA4D;IACtE,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QACxD,IAAI,CAAC,MAAM,CAAC,kCAAkC,EAAE,IAAA,yBAAe,EAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC,CAAA;QAE1G,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,mCAAmC,EAAE,MAAM,EAAE,iBAAiB,CAAC,CAAA;YAC3E,MAAM,IAAI,GAAG,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAA;YACzC,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;YAClC,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,+BAA+B,CAAC,CAAA;QAC/H,CAAC;QAED,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,mCAAmC;YAC5C,OAAO,EAAE,+DAA+D;YACxE,MAAM,EAAE,IAAI;SACb,CAAC,CAAA;QAEF,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,CAAC,2GAA2G,CAAC,CAAA;IAC5H,CAAC;CACF,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;CAOd,CAAA;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC;;uFAEiF;QACnF,CAAC,CAAC,6DAA6D,CAAA;IACjE,OAAO;;;EAGP,KAAK,CAAC,CAAC,CAAC,4CAA4C,CAAC,CAAC,CAAC,EAAE;;uBAEpC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;;;;;;;;;kCASb,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa;;;EAGrE,IAAI;;;CAGL,CAAA;AACD,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC;iGAC2F;QAC7F,CAAC,CAAC,wDAAwD,CAAA;IAC5D,OAAO;;;EAGP,KAAK,CAAC,CAAC,CAAC,4CAA4C,CAAC,CAAC,CAAC,EAAE;;uBAEpC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;;;;;;;;;kCASb,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa;;;EAGrE,IAAI;;;CAGL,CAAA;AACD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const audit: Capability;
3
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/capabilities/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,KAAK,EAAE,UAwBnB,CAAA"}
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.audit = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.audit = {
6
+ name: "audit",
7
+ requires: ["events"],
8
+ describe: "Immutable who-did-what-to-what trail, derived from events",
9
+ apply(ctx, plan) {
10
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "audit", { worker: true });
11
+ plan.create("scripts/migrations.d/audit.ts", (0, helpers_1.migrationModule)("audit"), "audit migrations wiring");
12
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
13
+ if (ctx.hasWorker()) {
14
+ plan.create("apps/worker/src/audit-rules.ts", rulesFile(notes), "audit rule definitions");
15
+ plan.create("apps/worker/src/dispatch.d/audit.ts", `export const consumer = { name: "audit", events: ["*"] }\n`, "register audit as an event consumer");
16
+ plan.create("apps/worker/src/consumers.d/audit.ts", WORKER, "audit event consumer tick");
17
+ }
18
+ else {
19
+ plan.create("apps/api/src/platform/audit-rules.ts", rulesFile(notes), "audit rules (add a worker to consume them)");
20
+ }
21
+ plan.patchManifest({ platform: { audit: true } });
22
+ plan.nextStep("Run `pnpm migrate`. Edit audit rules in apps/worker/src/audit-rules.ts.");
23
+ },
24
+ };
25
+ function rulesFile(notes) {
26
+ const rules = notes
27
+ ? ` defineAuditRule({
28
+ event: "note.created",
29
+ action: "note.created",
30
+ target: (e) => ({ type: "note", id: idOf(e) }),
31
+ summary: () => "created a note",
32
+ }),
33
+ defineAuditRule({
34
+ event: "note.updated",
35
+ action: "note.updated",
36
+ target: (e) => ({ type: "note", id: idOf(e) }),
37
+ summary: () => "updated a note",
38
+ }),
39
+ defineAuditRule({
40
+ event: "note.deleted",
41
+ action: "note.deleted",
42
+ target: (e) => ({ type: "note", id: idOf(e) }),
43
+ summary: () => "deleted a note",
44
+ }),`
45
+ : ` // defineAuditRule({
46
+ // event: "thing.created", action: "thing.created",
47
+ // target: (e) => ({ type: "thing", id: idOf(e) }), summary: () => "created a thing",
48
+ // }),`;
49
+ return `import { createRuleSet, defineAuditRule, type AuditEvent } from "@obh/audit"
50
+
51
+ const idOf = (e: AuditEvent): string => String((e.payload as { id?: string }).id ?? "?")
52
+
53
+ // Map domain events to immutable audit entries.
54
+ export const auditRules = createRuleSet([
55
+ ${rules}
56
+ ])
57
+ `;
58
+ }
59
+ const WORKER = `// Claims the "audit" deliveries and records immutable audit entries.
60
+ import { createAuditWorker, pgAdapter } from "@obh/audit"
61
+ import { auditRules } from "../audit-rules"
62
+ import type { WorkerContext } from "../context"
63
+
64
+ let worker: ReturnType<typeof createAuditWorker>
65
+
66
+ export function init(ctx: WorkerContext): void {
67
+ worker = createAuditWorker({ db: pgAdapter(ctx.pool), rules: auditRules, instanceId: "worker" })
68
+ }
69
+
70
+ export async function tick(): Promise<void> {
71
+ await worker.tick()
72
+ }
73
+ `;
74
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/capabilities/audit.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,KAAK,GAAe;IAC/B,IAAI,EAAE,OAAO;IACb,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,2DAA2D;IACrE,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACxD,IAAI,CAAC,MAAM,CAAC,+BAA+B,EAAE,IAAA,yBAAe,EAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC,CAAA;QAEjG,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,wBAAwB,CAAC,CAAA;YACzF,IAAI,CAAC,MAAM,CACT,qCAAqC,EACrC,4DAA4D,EAC5D,qCAAqC,CACtC,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,MAAM,EAAE,2BAA2B,CAAC,CAAA;QAC1F,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,4CAA4C,CAAC,CAAA;QACrH,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,QAAQ,CAAC,yEAAyE,CAAC,CAAA;IAC1F,CAAC;CACF,CAAA;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,KAAK,GAAG,KAAK;QACjB,CAAC,CAAC;;;;;;;;;;;;;;;;;MAiBA;QACF,CAAC,CAAC;;;SAGG,CAAA;IACP,OAAO;;;;;;EAMP,KAAK;;CAEN,CAAA;AACD,CAAC;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;CAcd,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const entitlements: Capability;
3
+ //# sourceMappingURL=entitlements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/capabilities/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,YAAY,EAAE,UAe1B,CAAA"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.entitlements = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.entitlements = {
6
+ name: "entitlements",
7
+ describe: "Entitlements — feature access + numeric limits, enforced before the write",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "entitlements", { api: true });
10
+ plan.create("scripts/migrations.d/entitlements.ts", (0, helpers_1.migrationModule)("entitlements"), "entitlements migrations wiring");
11
+ if (ctx.hasApp("api")) {
12
+ plan.create("apps/api/src/platform/entitlements.ts", registryFile((0, helpers_1.hasNotesExample)(ctx)), "entitlements registry + client + guard");
13
+ plan.create("apps/api/src/routes/entitlements.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "read a workspace's entitlements");
14
+ }
15
+ plan.patchManifest({ platform: { entitlements: true } });
16
+ plan.nextStep("Run `pnpm migrate`. Grant plans/overrides, then guard writes with entitlements.require(db, workspaceId, key).");
17
+ },
18
+ };
19
+ function registryFile(notes) {
20
+ const def = notes
21
+ ? `// A numeric limit with a code default; grant a higher value per plan or workspace.
22
+ export const notesMax = defineEntitlement({
23
+ key: "notes.max",
24
+ type: "number",
25
+ defaultValue: 100,
26
+ })`
27
+ : `// A boolean feature flag with a code default; grant true per plan or workspace.
28
+ export const exampleFeature = defineEntitlement({
29
+ key: "app.example_feature",
30
+ type: "boolean",
31
+ defaultValue: false,
32
+ })`;
33
+ const exportName = notes ? "notesMax" : "exampleFeature";
34
+ const guard = notes
35
+ ? `
36
+ /** Throws unless the workspace is under its notes.max entitlement. */
37
+ export async function requireCanCreateNote(): Promise<void> {
38
+ const max = await entitlements.limit(entitlementsDb, WORKSPACE, notesMax.key)
39
+ const { rows } = await pool.query<{ count: string }>("select count(*)::int as count from notes")
40
+ if (Number(rows[0].count) >= max) {
41
+ throw new Error(\`workspace \${WORKSPACE} has reached its notes limit (\${max})\`)
42
+ }
43
+ }
44
+ `
45
+ : `
46
+ /** Throws EntitlementRequiredError unless the workspace has the feature. */
47
+ export async function requireExampleFeature(): Promise<void> {
48
+ await entitlements.require(entitlementsDb, WORKSPACE, exampleFeature.key)
49
+ }
50
+ `;
51
+ return `import { createEntitlementRegistry, createEntitlementsClient, defineEntitlement, pgAdapter } from "@obh/entitlements"
52
+ import { pool } from "../db"
53
+
54
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
55
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
56
+
57
+ ${def}
58
+
59
+ export const registry = createEntitlementRegistry([${exportName}])
60
+
61
+ // The client is stateless; each call takes a db/tx handle and a workspace id.
62
+ export const entitlements = createEntitlementsClient({ registry })
63
+ export const entitlementsDb = pgAdapter(pool)
64
+ ${guard}`;
65
+ }
66
+ const ROUTE_HONO = `import type { Hono } from "hono"
67
+ import { entitlements, entitlementsDb, WORKSPACE } from "../platform/entitlements"
68
+
69
+ export function register(app: Hono): void {
70
+ // All effective entitlements for the workspace (value + where it resolved from).
71
+ app.get("/entitlements", async (c) => {
72
+ return c.json(await entitlements.listEffective(entitlementsDb, WORKSPACE))
73
+ })
74
+
75
+ // Explain a single entitlement: effective value plus the full fallback chain.
76
+ app.get("/entitlements/:key", async (c) => {
77
+ return c.json(await entitlements.explain(entitlementsDb, WORKSPACE, c.req.param("key")))
78
+ })
79
+ }
80
+ `;
81
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
82
+ import { entitlements, entitlementsDb, WORKSPACE } from "../platform/entitlements"
83
+
84
+ export function register(app: Express): void {
85
+ app.get("/entitlements", async (_req: Request, res: Response) => {
86
+ res.json(await entitlements.listEffective(entitlementsDb, WORKSPACE))
87
+ })
88
+
89
+ app.get("/entitlements/:key", async (req: Request, res: Response) => {
90
+ res.json(await entitlements.explain(entitlementsDb, WORKSPACE, req.params.key))
91
+ })
92
+ }
93
+ `;
94
+ //# sourceMappingURL=entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.js","sourceRoot":"","sources":["../../src/capabilities/entitlements.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,YAAY,GAAe;IACtC,IAAI,EAAE,cAAc;IACpB,QAAQ,EAAE,2EAA2E;IACrF,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5D,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,IAAA,yBAAe,EAAC,cAAc,CAAC,EAAE,gCAAgC,CAAC,CAAA;QAEtH,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,YAAY,CAAC,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC,EAAE,wCAAwC,CAAC,CAAA;YAClI,IAAI,CAAC,MAAM,CAAC,qCAAqC,EAAE,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EAAE,iCAAiC,CAAC,CAAA;QAClJ,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACxD,IAAI,CAAC,QAAQ,CAAC,+GAA+G,CAAC,CAAA;IAChI,CAAC;CACF,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,GAAG,GAAG,KAAK;QACf,CAAC,CAAC;;;;;GAKH;QACC,CAAC,CAAC;;;;;GAKH,CAAA;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAA;IACxD,MAAM,KAAK,GAAG,KAAK;QACjB,CAAC,CAAC;;;;;;;;;CASL;QACG,CAAC,CAAC;;;;;CAKL,CAAA;IACC,OAAO;;;;;;EAMP,GAAG;;qDAEgD,UAAU;;;;;EAK7D,KAAK,EAAE,CAAA;AACT,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;;;;;CAclB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;;;CAYrB,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const events: Capability;
3
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/capabilities/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,MAAM,EAAE,UAsBpB,CAAA"}