@oimlsmart/platform-server 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.
- package/README.md +92 -0
- package/migrations/0001_init.sql +69 -0
- package/migrations/0002_identity.sql +24 -0
- package/migrations/0003_federation_peers.sql +21 -0
- package/migrations/0003_users_rbac.sql +8 -0
- package/migrations/0004_oidc_op.sql +61 -0
- package/migrations/0005_upstream_providers.sql +34 -0
- package/migrations/0006_op_accounts.sql +28 -0
- package/migrations/0007_org_join_requests.sql +26 -0
- package/migrations/0008_op_client_roles.sql +19 -0
- package/migrations/0009_account_console.sql +32 -0
- package/migrations/0009_sso_states.sql +13 -0
- package/migrations/0010_notify_events.sql +23 -0
- package/migrations/0011_op_launch.sql +18 -0
- package/migrations/0011_org_memberships.sql +62 -0
- package/migrations/0012_notify_subscriptions.sql +57 -0
- package/migrations/0012_strong_auth.sql +109 -0
- package/migrations/0013_org_registry.sql +53 -0
- package/migrations/0014_notify_inbox.sql +34 -0
- package/migrations/0015_certificate_holder_attribution.sql +63 -0
- package/migrations/0016_instrument_registrations.sql +78 -0
- package/package.json +52 -0
- package/src/client-info.ts +25 -0
- package/src/context.ts +31 -0
- package/src/github.ts +284 -0
- package/src/mailer.ts +309 -0
- package/src/oidc.ts +369 -0
- package/src/profile/node.ts +83 -0
- package/src/profile.ts +582 -0
- package/src/rbac/node.ts +42 -0
- package/src/rbac.ts +53 -0
- package/src/session.ts +45 -0
- package/src/store/d1.ts +2850 -0
- package/src/store/sqlite/entities.ts +71 -0
- package/src/store/sqlite/events.ts +82 -0
- package/src/store/sqlite/factors-store.ts +348 -0
- package/src/store/sqlite/notify.ts +247 -0
- package/src/store/sqlite/op-accounts-store.ts +470 -0
- package/src/store/sqlite/op-store.ts +280 -0
- package/src/store/sqlite/schema.sql +745 -0
- package/src/store/sqlite/store.ts +1390 -0
- package/src/store/sqlite/upstream-store.ts +148 -0
- package/src/store/sqlite.ts +1027 -0
- package/src/store.ts +1826 -0
- package/src/vocab/index.ts +12 -0
- package/src/vocab/permissions.ts +398 -0
- package/src/vocab/rbac.ts +281 -0
- package/src/vocab/roles.ts +162 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @oimlsmart/platform-server
|
|
2
|
+
|
|
3
|
+
The OIML SMART platform server kernel: the modules the certification
|
|
4
|
+
platform (github.com/oimlsmart/smart) and the identity service
|
|
5
|
+
(github.com/oimlsmart/identity) both consume, published once to npm so
|
|
6
|
+
the two repositories never fork them. The boundary is measured, not
|
|
7
|
+
aspirational: PROGRESS/41-identity-extraction-map.md §2 in the smart
|
|
8
|
+
repository. Both consumers pin the published semver line; the version
|
|
9
|
+
pin IS the contract (TODO.repos/01).
|
|
10
|
+
|
|
11
|
+
## What it is
|
|
12
|
+
|
|
13
|
+
- `store`, the ServerStore seam: the interface, every row type, the
|
|
14
|
+
demo-account plan, `installStore`/`getStore`. Worker-safe.
|
|
15
|
+
- `store/d1`, the Cloudflare D1 implementation. Worker-safe.
|
|
16
|
+
- `store/sqlite`, the node implementation (better-sqlite3): the server
|
|
17
|
+
store composed from the sync modules, the entity-store verbs, the raw
|
|
18
|
+
driver handle, the schema (`SQLITE_SCHEMA_PATH`), and the migration
|
|
19
|
+
set's location (`MIGRATIONS_DIR`). Node-only.
|
|
20
|
+
- `migrations/`, the canonical D1 migration set. Both deployments point
|
|
21
|
+
wrangler's `migrations_dir` at this directory in the consumer's
|
|
22
|
+
node_modules; the live databases' journals key on the filenames.
|
|
23
|
+
- `profile`, the deployment profile model (hub / IA / TL / identity):
|
|
24
|
+
parse, resolve, install, the account seed plan. Worker-safe.
|
|
25
|
+
- `profile/node`, the file/env profile loader. Node-only (fs).
|
|
26
|
+
- `mailer`, the transactional mail seam (provider dispatch + the D1
|
|
27
|
+
outbox). Worker-safe.
|
|
28
|
+
- `rbac`, the instance's effective role to permission map
|
|
29
|
+
(`installRbacMap` / `effectiveRbacMap`). Worker-safe.
|
|
30
|
+
- `rbac/node`, the map's node carriers (INSTANCE_RBAC_JSON, the
|
|
31
|
+
profile file's `rbac:` section). Node-only (fs).
|
|
32
|
+
- `oidc`, the OIDC relying-party client (discovery, PKCE, the code
|
|
33
|
+
exchange, ID-token validation). Worker-safe.
|
|
34
|
+
- `github`, the GitHub OAuth cone (state sign/verify, the authorized
|
|
35
|
+
users declaration, the org-membership check). Worker-safe.
|
|
36
|
+
- `session`, the `oiml-session` cookie seam: `SESSION_COOKIE`,
|
|
37
|
+
`sessionUser(c)`, `sessionCookieOpts(c)`. Worker-safe.
|
|
38
|
+
- `client-info`, the request's client context (user agent, IP).
|
|
39
|
+
Worker-safe.
|
|
40
|
+
- `vocab`, the shared identity vocabulary: the role model, the role to
|
|
41
|
+
permission map, the action-permission catalog. Isomorphic.
|
|
42
|
+
|
|
43
|
+
The worker-safe rule, at package granularity: every subpath except
|
|
44
|
+
`store/sqlite`, `profile/node` and `rbac/node` carries no node
|
|
45
|
+
built-ins; a Worker bundle never imports the three node subpaths.
|
|
46
|
+
|
|
47
|
+
## The migration contract
|
|
48
|
+
|
|
49
|
+
`migrations/` is the ONE schema-migration set both deployments apply
|
|
50
|
+
(the smart platform's D1 databases and the identity service's account
|
|
51
|
+
registry alike):
|
|
52
|
+
|
|
53
|
+
- **Expand-only, filename-keyed.** A new schema act appends
|
|
54
|
+
`NNNN_name.sql`; existing files never change and never renumber.
|
|
55
|
+
wrangler records applied filenames in each database's
|
|
56
|
+
`d1_migrations` journal, so a rename or an edit would split the live
|
|
57
|
+
history.
|
|
58
|
+
- Duplicate sequence numbers are legal siblings (the wave that landed
|
|
59
|
+
two stores the same week); apply order is the filename sort.
|
|
60
|
+
- `test/migrations.test.ts` pins the set's end state to
|
|
61
|
+
`src/store/sqlite/schema.sql` (table and column sets), so the SQLite
|
|
62
|
+
DDL and the D1 journal path can never drift apart inside the
|
|
63
|
+
package.
|
|
64
|
+
- Consumers read the set's location from `MIGRATIONS_DIR`
|
|
65
|
+
(`@oimlsmart/platform-server/store/sqlite`); deploy configs point at
|
|
66
|
+
`node_modules/@oimlsmart/platform-server/migrations`.
|
|
67
|
+
|
|
68
|
+
## Source-form exports
|
|
69
|
+
|
|
70
|
+
The package ships its TypeScript sources directly (the
|
|
71
|
+
`@oimlsmart/site-shell` pattern, no build step): the consumers'
|
|
72
|
+
toolchains (vite/astro, tsx, vitest, vue-tsc) compile them in place.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
npm ci
|
|
78
|
+
npm run typecheck
|
|
79
|
+
npm test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Releasing
|
|
83
|
+
|
|
84
|
+
Trusted publishing rides `v*` tags on this repository (the
|
|
85
|
+
`.github/workflows/release.yml` machinery, npm OIDC, no stored token):
|
|
86
|
+
bump `version` in `package.json` on `main` (the `package-lock.json`
|
|
87
|
+
root fields bump with it), tag `v<x.y.z>` at the merge commit, push
|
|
88
|
+
the tag. The workflow runs the gates, refuses a tag that does not
|
|
89
|
+
equal the package version, and publishes with `--provenance`. The
|
|
90
|
+
one-time bootstrap (the manual first publish + the npmjs.com trusted
|
|
91
|
+
publisher enrollment) is done; the runbook is
|
|
92
|
+
docs/deployment/npm-releases.md in the smart repository.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
-- Migration 0001 — the platform schema (TODO.cs-e2e/14).
|
|
2
|
+
-- Mirrors server/db/schema.sql STATEMENT-FOR-STATEMENT (the node
|
|
3
|
+
-- posture applies schema.sql at boot; the D1 posture applies this
|
|
4
|
+
-- migration via `wrangler d1 migrations apply`). When schema.sql
|
|
5
|
+
-- changes, add the next migration here AND keep schema.sql current —
|
|
6
|
+
-- src/__tests__/d1-store.test.ts pins the two to the same CREATE set.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
email TEXT UNIQUE NOT NULL,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
avatar_url TEXT,
|
|
12
|
+
provider TEXT NOT NULL DEFAULT 'demo',
|
|
13
|
+
provider_account_id TEXT,
|
|
14
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
15
|
+
-- Organization linkage: manufacturer id (applicant), IA oiml_code (ia_officer),
|
|
16
|
+
-- TL oiml_id (tl_operator); NULL for cs_admin/admin/viewer.
|
|
17
|
+
org_id TEXT,
|
|
18
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
19
|
+
last_login TEXT
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
25
|
+
token TEXT UNIQUE NOT NULL,
|
|
26
|
+
expires_at TEXT NOT NULL,
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- The workflow entity store (TODO.ops/07 — server-side persistence):
|
|
31
|
+
-- one JSON document per entity, keyed by (store, id) — the same shape
|
|
32
|
+
-- the browser's IndexedDB stores hold, so the two repository backends
|
|
33
|
+
-- are contract-identical. org_id carries the scoping column when the
|
|
34
|
+
-- entity declares one (server-side enforcement for org-bound roles).
|
|
35
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
36
|
+
store TEXT NOT NULL,
|
|
37
|
+
id TEXT NOT NULL,
|
|
38
|
+
org_id TEXT,
|
|
39
|
+
data TEXT NOT NULL,
|
|
40
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
PRIMARY KEY (store, id)
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_entities_store_org ON entities (store, org_id);
|
|
44
|
+
|
|
45
|
+
-- The change journal: every write appends (seq, store, type, id) —
|
|
46
|
+
-- the SSE stream tails it (each client filters its stores).
|
|
47
|
+
CREATE TABLE IF NOT EXISTS entity_changes (
|
|
48
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
store TEXT NOT NULL,
|
|
50
|
+
type TEXT NOT NULL,
|
|
51
|
+
id TEXT NOT NULL,
|
|
52
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- The evidence store (TODO.ops/09 — the monitor daemon's durable
|
|
56
|
+
-- streams): append-only records across restarts. The adapter contract
|
|
57
|
+
-- (src/evidence-store/adapter.ts) is tiny: append, query, getByIds,
|
|
58
|
+
-- counts.
|
|
59
|
+
CREATE TABLE IF NOT EXISTS evidence_records (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
kind TEXT NOT NULL,
|
|
62
|
+
twin_id TEXT,
|
|
63
|
+
monitor_id TEXT,
|
|
64
|
+
at TEXT NOT NULL,
|
|
65
|
+
data TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_kind_at ON evidence_records (kind, at);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_twin ON evidence_records (twin_id, at);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_monitor ON evidence_records (monitor_id, at);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Migration 0002 — identity federation (TODO.federation/10).
|
|
2
|
+
-- Adds the RP-initiated-logout hint to sessions and the SSO approval
|
|
3
|
+
-- queue. schema.sql carries the same end state for fresh databases —
|
|
4
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
5
|
+
-- schema.sql's CREATE set.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE sessions ADD COLUMN id_token_hint TEXT;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS identity_approvals (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
email TEXT NOT NULL,
|
|
12
|
+
name TEXT NOT NULL,
|
|
13
|
+
issuer TEXT NOT NULL,
|
|
14
|
+
sub TEXT NOT NULL,
|
|
15
|
+
claims_json TEXT,
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
17
|
+
decided_role TEXT,
|
|
18
|
+
decided_org TEXT,
|
|
19
|
+
decided_by TEXT,
|
|
20
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
21
|
+
last_seen TEXT,
|
|
22
|
+
decided_at TEXT,
|
|
23
|
+
UNIQUE (issuer, sub)
|
|
24
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Migration 0003 — federation peers (TODO.federation/04).
|
|
2
|
+
-- The peer registry: pinned counterparty instances (descriptor + trust
|
|
3
|
+
-- posture). schema.sql carries the same end state for fresh databases —
|
|
4
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
5
|
+
-- schema.sql's CREATE set.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS federation_peers (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
roles TEXT NOT NULL,
|
|
11
|
+
descriptor_url TEXT,
|
|
12
|
+
descriptor_json TEXT NOT NULL,
|
|
13
|
+
pinned_via TEXT NOT NULL DEFAULT 'url',
|
|
14
|
+
connectivity TEXT NOT NULL DEFAULT 'verified',
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
16
|
+
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
added_by TEXT,
|
|
18
|
+
refreshed_at TEXT,
|
|
19
|
+
revoked_at TEXT,
|
|
20
|
+
revoked_by TEXT
|
|
21
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Migration 0003 — the RBAC user columns (TODO.federation/12).
|
|
2
|
+
-- schema.sql (fresh node DBs) carries these columns in the users CREATE;
|
|
3
|
+
-- THIS migration upgrades D1 databases created from 0001+0002. The node
|
|
4
|
+
-- store's migrateAuthTables and the D1 store's column probe apply the
|
|
5
|
+
-- same adds defensively (a dev database migrated before this file
|
|
6
|
+
-- landed).
|
|
7
|
+
ALTER TABLE users ADD COLUMN roles TEXT;
|
|
8
|
+
ALTER TABLE users ADD COLUMN active INTEGER NOT NULL DEFAULT 1;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- Migration 0004 — the OIDC Provider (TODO.identity/01).
|
|
2
|
+
-- Adds the OP's D1-backed state: the client registry, the pending
|
|
3
|
+
-- authorizations, the one-time codes, the access tokens, and the
|
|
4
|
+
-- signing-key rotation history (public halves only). schema.sql carries
|
|
5
|
+
-- the same end state for fresh databases — src/__tests__/d1-store.test.ts
|
|
6
|
+
-- pins the UNION of every migration to schema.sql's CREATE set.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS oidc_clients (
|
|
9
|
+
client_id TEXT PRIMARY KEY,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
secret_hash TEXT,
|
|
12
|
+
redirect_uris TEXT NOT NULL,
|
|
13
|
+
claims_policy TEXT,
|
|
14
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
created_by TEXT
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS oidc_authorizations (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
client_id TEXT NOT NULL,
|
|
22
|
+
redirect_uri TEXT NOT NULL,
|
|
23
|
+
scope TEXT NOT NULL,
|
|
24
|
+
state TEXT NOT NULL,
|
|
25
|
+
nonce TEXT,
|
|
26
|
+
code_challenge TEXT NOT NULL,
|
|
27
|
+
user_id TEXT,
|
|
28
|
+
decision TEXT,
|
|
29
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
expires_at TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS oidc_codes (
|
|
34
|
+
code TEXT PRIMARY KEY,
|
|
35
|
+
client_id TEXT NOT NULL,
|
|
36
|
+
redirect_uri TEXT NOT NULL,
|
|
37
|
+
scope TEXT NOT NULL,
|
|
38
|
+
nonce TEXT,
|
|
39
|
+
code_challenge TEXT NOT NULL,
|
|
40
|
+
user_id TEXT NOT NULL,
|
|
41
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
42
|
+
expires_at TEXT NOT NULL,
|
|
43
|
+
consumed_at TEXT
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS oidc_access_tokens (
|
|
47
|
+
token TEXT PRIMARY KEY,
|
|
48
|
+
user_id TEXT NOT NULL,
|
|
49
|
+
client_id TEXT NOT NULL,
|
|
50
|
+
scope TEXT NOT NULL,
|
|
51
|
+
expires_at TEXT NOT NULL,
|
|
52
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS oidc_keys (
|
|
56
|
+
kid TEXT PRIMARY KEY,
|
|
57
|
+
public_jwk TEXT NOT NULL,
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
59
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
60
|
+
retired_at TEXT
|
|
61
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Migration 0005 — the upstream provider registry (TODO.identity/08).
|
|
2
|
+
-- Adds the OP's upstream sign-in registry (identity_providers: GitHub +
|
|
3
|
+
-- Google + Apple + Entra + generic OIDC rows, secrets by env reference)
|
|
4
|
+
-- and the linked identities (identity_links — TODO.identity/02's shape,
|
|
5
|
+
-- landed additively here because 08's flows consume them; THE MATCH
|
|
6
|
+
-- RULE: resolve by (provider, provider_account_id), never by email).
|
|
7
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
8
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
9
|
+
-- schema.sql's CREATE set.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS identity_providers (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
kind TEXT NOT NULL,
|
|
14
|
+
display_name TEXT NOT NULL,
|
|
15
|
+
brand_mark TEXT,
|
|
16
|
+
issuer TEXT,
|
|
17
|
+
client_id TEXT NOT NULL,
|
|
18
|
+
client_secret_ref TEXT,
|
|
19
|
+
scopes TEXT,
|
|
20
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
22
|
+
created_by TEXT,
|
|
23
|
+
updated_at TEXT
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS identity_links (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
29
|
+
provider TEXT NOT NULL,
|
|
30
|
+
provider_account_id TEXT NOT NULL,
|
|
31
|
+
linked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
32
|
+
linked_by TEXT,
|
|
33
|
+
UNIQUE (provider, provider_account_id)
|
|
34
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Migration 0006 — the OP's account model (TODO.identity/02).
|
|
2
|
+
-- Adds the password credentials (apart from the users row, so no
|
|
3
|
+
-- SELECT * ever reads credential material) and the invite-only
|
|
4
|
+
-- enrollment tokens (one-time, 24 h, atomically consumed). The linked
|
|
5
|
+
-- identities table itself landed with 0005_upstream_providers (08's
|
|
6
|
+
-- flows consume it); this migration adds the per-user index on it.
|
|
7
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
8
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
9
|
+
-- schema.sql's CREATE set.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS passwords (
|
|
12
|
+
user_id TEXT PRIMARY KEY REFERENCES users(id),
|
|
13
|
+
hash TEXT NOT NULL,
|
|
14
|
+
set_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
|
+
set_by TEXT
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS enrollment_tokens (
|
|
19
|
+
token TEXT PRIMARY KEY,
|
|
20
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
21
|
+
created_by TEXT,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
expires_at TEXT NOT NULL,
|
|
24
|
+
consumed_at TEXT
|
|
25
|
+
);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_enrollment_tokens_user ON enrollment_tokens (user_id);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_identity_links_user ON identity_links (user_id);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- TODO.identity/10 — delegated organization administration: the
|
|
2
|
+
-- self-service join requests. org_id set = a registered participant
|
|
3
|
+
-- org's queue (its org admin decides); org_id NULL + org_name_text =
|
|
4
|
+
-- the "my organization is not listed" path (BIML's new-organizations
|
|
5
|
+
-- queue). The decision is atomic on status='pending'; approval records
|
|
6
|
+
-- the invited account (invited_user_id).
|
|
7
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
8
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
9
|
+
-- schema.sql's CREATE set.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS org_join_requests (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
name TEXT NOT NULL,
|
|
14
|
+
email TEXT NOT NULL,
|
|
15
|
+
org_id TEXT,
|
|
16
|
+
org_name_text TEXT,
|
|
17
|
+
requested_role TEXT NOT NULL,
|
|
18
|
+
note TEXT,
|
|
19
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
20
|
+
decided_by TEXT,
|
|
21
|
+
decided_at TEXT,
|
|
22
|
+
refusal_reason TEXT,
|
|
23
|
+
invited_user_id TEXT,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
+
);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_org_join_requests_org ON org_join_requests (org_id, status);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Migration 0008 — the central user registry's PER-CLIENT role
|
|
2
|
+
-- assignments (TODO.identity/03). One row per (user, client): the ID
|
|
3
|
+
-- token issued to that client carries these roles (filtered by the
|
|
4
|
+
-- client's claims-policy role allowlist); no row = the account's
|
|
5
|
+
-- OP-side role set is the federation-wide default (the pre-03
|
|
6
|
+
-- behavior); roles='[]' = explicitly no roles on this client.
|
|
7
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
8
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
9
|
+
-- schema.sql's CREATE set.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS op_client_roles (
|
|
12
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
13
|
+
client_id TEXT NOT NULL,
|
|
14
|
+
roles TEXT NOT NULL,
|
|
15
|
+
assigned_by TEXT,
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
updated_at TEXT,
|
|
18
|
+
PRIMARY KEY (user_id, client_id)
|
|
19
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- Migration 0009 — the account-holder console (TODO.identity/06).
|
|
2
|
+
-- (The 0009 prefix is shared with 0009_sso_states.sql (TODO.identity/04);
|
|
3
|
+
-- the D1 migrations journal keys on the FILE NAME — the double-0003
|
|
4
|
+
-- precedent — and the two touch disjoint tables/columns. The name must
|
|
5
|
+
-- stay exactly this: the shared preview D1 applied it under this name,
|
|
6
|
+
-- and a rename re-runs the ALTERs there into duplicate-column errors.)
|
|
7
|
+
-- users.email_verified_at — the primary address's verification state
|
|
8
|
+
-- sessions.user_agent / ip / last_seen_at — the sign-in context the
|
|
9
|
+
-- sessions section lists
|
|
10
|
+
-- email_change_tokens — the verify-new-email ceremony (one-time,
|
|
11
|
+
-- 24 h, atomically consumed; delivered_by
|
|
12
|
+
-- records the mailer vs on-screen channel)
|
|
13
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
14
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
15
|
+
-- schema.sql's CREATE set.
|
|
16
|
+
|
|
17
|
+
ALTER TABLE users ADD COLUMN email_verified_at TEXT;
|
|
18
|
+
|
|
19
|
+
ALTER TABLE sessions ADD COLUMN user_agent TEXT;
|
|
20
|
+
ALTER TABLE sessions ADD COLUMN ip TEXT;
|
|
21
|
+
ALTER TABLE sessions ADD COLUMN last_seen_at TEXT;
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS email_change_tokens (
|
|
24
|
+
token TEXT PRIMARY KEY,
|
|
25
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
26
|
+
new_email TEXT NOT NULL,
|
|
27
|
+
delivered_by TEXT NOT NULL DEFAULT 'shown',
|
|
28
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
29
|
+
expires_at TEXT NOT NULL,
|
|
30
|
+
consumed_at TEXT
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_email_change_tokens_user ON email_change_tokens (user_id);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- TODO.identity/04 — the relying party's OIDC sign-in state jar (the
|
|
2
|
+
-- /signin/oidc → /callback/oidc round trip's one-time state: the nonce +
|
|
3
|
+
-- the PKCE verifier). STORE-BACKED so the Worker's isolates share it —
|
|
4
|
+
-- the per-process Map intermittently failed the state check across
|
|
5
|
+
-- isolates (the GitHub-flow lesson, retired for SSO too).
|
|
6
|
+
CREATE TABLE IF NOT EXISTS sso_states (
|
|
7
|
+
state TEXT PRIMARY KEY,
|
|
8
|
+
nonce TEXT NOT NULL,
|
|
9
|
+
verifier TEXT NOT NULL,
|
|
10
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
11
|
+
expires_at TEXT NOT NULL,
|
|
12
|
+
consumed_at TEXT
|
|
13
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- TODO.notify/01 — the platform event store (the notification system's
|
|
2
|
+
-- source of truth): one row per DECLARED notifiable act (the catalog,
|
|
3
|
+
-- browser/src/notify/catalog.ts, is deliberate — not every write is an
|
|
4
|
+
-- event). The hierarchical key <domain>/<entity-id>/<action> is SPLIT
|
|
5
|
+
-- into columns so the subscription grammar's prefixes resolve in SQL
|
|
6
|
+
-- (WHERE domain = ? AND entity_id = ?), never a string scan; the
|
|
7
|
+
-- composed key is derived at read. payload is the catalog row's JSON
|
|
8
|
+
-- envelope (the summary line, the deep link, the actors, the entity's
|
|
9
|
+
-- store for the read-time visibility gate). seq is the feed cursor;
|
|
10
|
+
-- written inside the acting request's envelope, never blocking the
|
|
11
|
+
-- triggering flow (the mailer doctrine). The d1-store suite's drift
|
|
12
|
+
-- tripwire pins this migration set's end state to schema.sql.
|
|
13
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
14
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
id TEXT UNIQUE NOT NULL,
|
|
16
|
+
domain TEXT NOT NULL,
|
|
17
|
+
entity_id TEXT NOT NULL,
|
|
18
|
+
action TEXT NOT NULL,
|
|
19
|
+
payload TEXT NOT NULL,
|
|
20
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_events_domain_entity ON events (domain, entity_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_events_domain_action ON events (domain, action);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Migration 0011 — the SSO home's launch metadata on the OIDC client
|
|
2
|
+
-- registry (the identity service's post-login launcher: the spec is
|
|
3
|
+
-- TODO.identity-extract/02a's "post-login landing"). One set of columns
|
|
4
|
+
-- per client: the launch URL (the service's sign-in start; NULL = the
|
|
5
|
+
-- client never appears on the launcher), the icon glyph's name, the
|
|
6
|
+
-- one-line description, and the visibility rule for an account the
|
|
7
|
+
-- computed role set does not admit ('roles' hides the card, 'request'
|
|
8
|
+
-- shows it with the plain request-access state, 'open' never gates).
|
|
9
|
+
-- Numbering: 0010 is the platform's notify wave; the identity repo's
|
|
10
|
+
-- byte-identical set continues the shared numbering here.
|
|
11
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
12
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
13
|
+
-- schema.sql's CREATE set.
|
|
14
|
+
|
|
15
|
+
ALTER TABLE oidc_clients ADD COLUMN launch_url TEXT;
|
|
16
|
+
ALTER TABLE oidc_clients ADD COLUMN launch_icon TEXT;
|
|
17
|
+
ALTER TABLE oidc_clients ADD COLUMN launch_description TEXT;
|
|
18
|
+
ALTER TABLE oidc_clients ADD COLUMN launch_visibility TEXT NOT NULL DEFAULT 'roles';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- Migration 0011 — the multi-organization membership model
|
|
2
|
+
-- (TODO.identity/11). An account can belong to SEVERAL organizations and
|
|
3
|
+
-- acts AS one at a time (the GitHub context-switch pattern): the
|
|
4
|
+
-- org_memberships table carries the account × org × per-org role set with
|
|
5
|
+
-- the lifecycle state (invited → active ⇄ disabled); the session's
|
|
6
|
+
-- active_org stamp names the context the account acts under; the OIDC
|
|
7
|
+
-- code + access token carry the context the consent was given under (the
|
|
8
|
+
-- token endpoint re-judges it against the live membership, so a
|
|
9
|
+
-- membership disabled mid-flow never emits a dead org's claims).
|
|
10
|
+
--
|
|
11
|
+
-- (The 0011 prefix is shared with 0011_op_launch.sql (the SSO home's
|
|
12
|
+
-- launch metadata); the D1 migrations journal keys on the FILE NAME —
|
|
13
|
+
-- the double-0003 and double-0009 precedents — and the two touch
|
|
14
|
+
-- disjoint tables/columns. The name must stay exactly this: a SHARED D1
|
|
15
|
+
-- applied it under this name (the preview registry), and a rename
|
|
16
|
+
-- re-runs the ALTERs there into duplicate-column errors.)
|
|
17
|
+
--
|
|
18
|
+
-- THE DUAL-READ DOCTRINE: the users row's org_id/roles columns stay the
|
|
19
|
+
-- BACKWARD-COMPATIBLE read — the PRIMARY membership's mirror — until
|
|
20
|
+
-- every consumer reads the memberships. The backfill below (idempotent;
|
|
21
|
+
-- the stores' defensive ensures carry its twin for pre-migration dev
|
|
22
|
+
-- databases) creates every org-bound account's primary membership from
|
|
23
|
+
-- the legacy columns, so the legacy read and the membership read agree
|
|
24
|
+
-- from the first request.
|
|
25
|
+
-- schema.sql carries the same end state for fresh databases —
|
|
26
|
+
-- src/__tests__/d1-store.test.ts pins the UNION of every migration to
|
|
27
|
+
-- schema.sql's CREATE set.
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS org_memberships (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
32
|
+
org_id TEXT NOT NULL,
|
|
33
|
+
-- The per-org role set (JSON array): the roles the account's tokens
|
|
34
|
+
-- carry when it acts AS this org.
|
|
35
|
+
roles TEXT NOT NULL DEFAULT '[]',
|
|
36
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
37
|
+
-- The PRIMARY membership mirrors the users row's org_id/roles (the
|
|
38
|
+
-- dual-read doctrine; one per account).
|
|
39
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
invited_by TEXT,
|
|
41
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
42
|
+
activated_at TEXT,
|
|
43
|
+
disabled_at TEXT,
|
|
44
|
+
disabled_by TEXT,
|
|
45
|
+
UNIQUE (user_id, org_id)
|
|
46
|
+
);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_org_memberships_org ON org_memberships (org_id, state);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_org_memberships_user ON org_memberships (user_id, state);
|
|
49
|
+
|
|
50
|
+
ALTER TABLE sessions ADD COLUMN active_org TEXT;
|
|
51
|
+
ALTER TABLE oidc_codes ADD COLUMN context_org TEXT;
|
|
52
|
+
ALTER TABLE oidc_access_tokens ADD COLUMN context_org TEXT;
|
|
53
|
+
|
|
54
|
+
-- The backfill: every org-bound account's primary membership, mirrored
|
|
55
|
+
-- from the legacy columns (the full legacy role set, or the primary role
|
|
56
|
+
-- when the set column is NULL). Idempotent (INSERT OR IGNORE + the
|
|
57
|
+
-- deterministic id), so the stores' defensive twin never duplicates.
|
|
58
|
+
INSERT OR IGNORE INTO org_memberships (id, user_id, org_id, roles, state, is_primary, activated_at)
|
|
59
|
+
SELECT 'mbr-' || id, id, org_id,
|
|
60
|
+
CASE WHEN roles IS NOT NULL AND roles != '' THEN roles ELSE json_array(role) END,
|
|
61
|
+
'active', 1, COALESCE(last_login, created_at)
|
|
62
|
+
FROM users WHERE org_id IS NOT NULL;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- TODO.notify/02 — the subscriptions store (the notification system's
|
|
2
|
+
-- per-user rules, the GitHub shape; TODO.notify/00's contract). Three
|
|
3
|
+
-- tables:
|
|
4
|
+
--
|
|
5
|
+
-- notify_rules: the per-user rule rows (mode subscribe|mute) on a key
|
|
6
|
+
-- pattern in the catalog's grammar (application/**, certificate/**/
|
|
7
|
+
-- issued, test-run/asg-…-001/**, or an exact key). The pattern's
|
|
8
|
+
-- pinned legs are SPLIT into columns (domain is always pinned;
|
|
9
|
+
-- entity_id/action NULL = the wild leg) so the recipient
|
|
10
|
+
-- resolution's REVERSE match — every user's rules covering one event —
|
|
11
|
+
-- resolves in SQL (WHERE domain = ? AND (entity_id IS NULL OR
|
|
12
|
+
-- entity_id = ?) AND (action IS NULL OR action = ?)), never a string
|
|
13
|
+
-- scan. channel_overrides is the subscribe row's per-rule email
|
|
14
|
+
-- override (JSON; NULL = the category preference rules on); a mute
|
|
15
|
+
-- row never carries one.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS notify_rules (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
19
|
+
pattern TEXT NOT NULL,
|
|
20
|
+
domain TEXT NOT NULL,
|
|
21
|
+
entity_id TEXT,
|
|
22
|
+
action TEXT,
|
|
23
|
+
mode TEXT NOT NULL,
|
|
24
|
+
channel_overrides TEXT,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
UNIQUE (user_id, pattern)
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_notify_rules_user ON notify_rules (user_id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_notify_rules_event ON notify_rules (domain, entity_id, action);
|
|
30
|
+
|
|
31
|
+
-- notify_entity_mutes: the thread-level mutes (the entity-page bell's
|
|
32
|
+
-- Muted state; the email footer's one-click unsubscribe — TODO.notify/
|
|
33
|
+
-- 04 — sets the same row). A mute wins over every candidate class,
|
|
34
|
+
-- subscriptions included; the access is unchanged.
|
|
35
|
+
CREATE TABLE IF NOT EXISTS notify_entity_mutes (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
38
|
+
domain TEXT NOT NULL,
|
|
39
|
+
entity_id TEXT NOT NULL,
|
|
40
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
UNIQUE (user_id, domain, entity_id)
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_notify_entity_mutes_entity ON notify_entity_mutes (domain, entity_id);
|
|
44
|
+
|
|
45
|
+
-- notify_preferences: one row per user — the per-category (the event
|
|
46
|
+
-- catalog's domains) email posture as a JSON map { "<domain>":
|
|
47
|
+
-- "immediate"|"digest"|"off" }. A domain ABSENT falls back to the
|
|
48
|
+
-- catalog row's own email default; the inbox always carries the
|
|
49
|
+
-- event. No row at all = every category on its catalog default.
|
|
50
|
+
CREATE TABLE IF NOT EXISTS notify_preferences (
|
|
51
|
+
user_id TEXT PRIMARY KEY REFERENCES users(id),
|
|
52
|
+
channels TEXT NOT NULL DEFAULT '{}',
|
|
53
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- The d1-store suite's drift tripwire pins this migration set's end
|
|
57
|
+
-- state to schema.sql (packages/platform-server/src/store/sqlite).
|