@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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/capabilities/analytics.d.ts +3 -0
- package/dist/capabilities/analytics.d.ts.map +1 -0
- package/dist/capabilities/analytics.js +91 -0
- package/dist/capabilities/analytics.js.map +1 -0
- package/dist/capabilities/api-keys.d.ts +3 -0
- package/dist/capabilities/api-keys.d.ts.map +1 -0
- package/dist/capabilities/api-keys.js +90 -0
- package/dist/capabilities/api-keys.js.map +1 -0
- package/dist/capabilities/audit.d.ts +3 -0
- package/dist/capabilities/audit.d.ts.map +1 -0
- package/dist/capabilities/audit.js +74 -0
- package/dist/capabilities/audit.js.map +1 -0
- package/dist/capabilities/entitlements.d.ts +3 -0
- package/dist/capabilities/entitlements.d.ts.map +1 -0
- package/dist/capabilities/entitlements.js +94 -0
- package/dist/capabilities/entitlements.js.map +1 -0
- package/dist/capabilities/events.d.ts +3 -0
- package/dist/capabilities/events.d.ts.map +1 -0
- package/dist/capabilities/events.js +107 -0
- package/dist/capabilities/events.js.map +1 -0
- package/dist/capabilities/files.d.ts +3 -0
- package/dist/capabilities/files.d.ts.map +1 -0
- package/dist/capabilities/files.js +100 -0
- package/dist/capabilities/files.js.map +1 -0
- package/dist/capabilities/helpers.d.ts +27 -0
- package/dist/capabilities/helpers.d.ts.map +1 -0
- package/dist/capabilities/helpers.js +63 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/import-export.d.ts +3 -0
- package/dist/capabilities/import-export.d.ts.map +1 -0
- package/dist/capabilities/import-export.js +267 -0
- package/dist/capabilities/import-export.js.map +1 -0
- package/dist/capabilities/index.d.ts +16 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +71 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/jobs.d.ts +3 -0
- package/dist/capabilities/jobs.d.ts.map +1 -0
- package/dist/capabilities/jobs.js +117 -0
- package/dist/capabilities/jobs.js.map +1 -0
- package/dist/capabilities/notifications.d.ts +3 -0
- package/dist/capabilities/notifications.d.ts.map +1 -0
- package/dist/capabilities/notifications.js +113 -0
- package/dist/capabilities/notifications.js.map +1 -0
- package/dist/capabilities/search.d.ts +3 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/search.js +121 -0
- package/dist/capabilities/search.js.map +1 -0
- package/dist/capabilities/settings.d.ts +3 -0
- package/dist/capabilities/settings.d.ts.map +1 -0
- package/dist/capabilities/settings.js +75 -0
- package/dist/capabilities/settings.js.map +1 -0
- package/dist/capabilities/types.d.ts +16 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +3 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capabilities/webhooks.d.ts +3 -0
- package/dist/capabilities/webhooks.d.ts.map +1 -0
- package/dist/capabilities/webhooks.js +117 -0
- package/dist/capabilities/webhooks.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +88 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +94 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +41 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +64 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/new.d.ts +18 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +149 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/shared.d.ts +14 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +36 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +59 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/stack.d.ts +13 -0
- package/dist/commands/stack.d.ts.map +1 -0
- package/dist/commands/stack.js +192 -0
- package/dist/commands/stack.js.map +1 -0
- package/dist/generators/api.d.ts +16 -0
- package/dist/generators/api.d.ts.map +1 -0
- package/dist/generators/api.js +375 -0
- package/dist/generators/api.js.map +1 -0
- package/dist/generators/lwd.d.ts +12 -0
- package/dist/generators/lwd.d.ts.map +1 -0
- package/dist/generators/lwd.js +143 -0
- package/dist/generators/lwd.js.map +1 -0
- package/dist/generators/mobile.d.ts +16 -0
- package/dist/generators/mobile.d.ts.map +1 -0
- package/dist/generators/mobile.js +180 -0
- package/dist/generators/mobile.js.map +1 -0
- package/dist/generators/platform-package.d.ts +18 -0
- package/dist/generators/platform-package.d.ts.map +1 -0
- package/dist/generators/platform-package.js +568 -0
- package/dist/generators/platform-package.js.map +1 -0
- package/dist/generators/root.d.ts +15 -0
- package/dist/generators/root.d.ts.map +1 -0
- package/dist/generators/root.js +249 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/sdk.d.ts +14 -0
- package/dist/generators/sdk.d.ts.map +1 -0
- package/dist/generators/sdk.js +137 -0
- package/dist/generators/sdk.js.map +1 -0
- package/dist/generators/web.d.ts +15 -0
- package/dist/generators/web.d.ts.map +1 -0
- package/dist/generators/web.js +203 -0
- package/dist/generators/web.js.map +1 -0
- package/dist/generators/worker.d.ts +13 -0
- package/dist/generators/worker.d.ts.map +1 -0
- package/dist/generators/worker.js +148 -0
- package/dist/generators/worker.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/project/context.d.ts +24 -0
- package/dist/project/context.d.ts.map +1 -0
- package/dist/project/context.js +63 -0
- package/dist/project/context.js.map +1 -0
- package/dist/project/manifest.d.ts +26 -0
- package/dist/project/manifest.d.ts.map +1 -0
- package/dist/project/manifest.js +73 -0
- package/dist/project/manifest.js.map +1 -0
- package/dist/project/paths.d.ts +25 -0
- package/dist/project/paths.d.ts.map +1 -0
- package/dist/project/paths.js +51 -0
- package/dist/project/paths.js.map +1 -0
- package/dist/project/plan.d.ts +42 -0
- package/dist/project/plan.d.ts.map +1 -0
- package/dist/project/plan.js +251 -0
- package/dist/project/plan.js.map +1 -0
- package/dist/prompts/index.d.ts +24 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +111 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/recipes/index.d.ts +25 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +77 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/stack/infer.d.ts +4 -0
- package/dist/stack/infer.d.ts.map +1 -0
- package/dist/stack/infer.js +165 -0
- package/dist/stack/infer.js.map +1 -0
- package/dist/stack/lwd.d.ts +24 -0
- package/dist/stack/lwd.d.ts.map +1 -0
- package/dist/stack/lwd.js +62 -0
- package/dist/stack/lwd.js.map +1 -0
- package/dist/stack/lwdtoml.d.ts +38 -0
- package/dist/stack/lwdtoml.d.ts.map +1 -0
- package/dist/stack/lwdtoml.js +57 -0
- package/dist/stack/lwdtoml.js.map +1 -0
- package/dist/stack/manifest.d.ts +15 -0
- package/dist/stack/manifest.d.ts.map +1 -0
- package/dist/stack/manifest.js +86 -0
- package/dist/stack/manifest.js.map +1 -0
- package/dist/stack/types.d.ts +48 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +5 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/stack/wire.d.ts +40 -0
- package/dist/stack/wire.d.ts.map +1 -0
- package/dist/stack/wire.js +192 -0
- package/dist/stack/wire.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +64 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +17 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +49 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +21 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +40 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +40 -0
- package/skills/obh-add-events/SKILL.md +26 -0
- package/skills/obh-generate-audit-rules/SKILL.md +26 -0
- package/skills/obh-inspect-project/SKILL.md +26 -0
- package/skills/obh-lwd-manifest/SKILL.md +26 -0
- package/skills/obh-retrofit-files/SKILL.md +24 -0
- package/skills/obh-retrofit-jobs/SKILL.md +26 -0
- package/skills/obh-sdk-extraction/SKILL.md +24 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|