@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.
Files changed (48) hide show
  1. package/README.md +92 -0
  2. package/migrations/0001_init.sql +69 -0
  3. package/migrations/0002_identity.sql +24 -0
  4. package/migrations/0003_federation_peers.sql +21 -0
  5. package/migrations/0003_users_rbac.sql +8 -0
  6. package/migrations/0004_oidc_op.sql +61 -0
  7. package/migrations/0005_upstream_providers.sql +34 -0
  8. package/migrations/0006_op_accounts.sql +28 -0
  9. package/migrations/0007_org_join_requests.sql +26 -0
  10. package/migrations/0008_op_client_roles.sql +19 -0
  11. package/migrations/0009_account_console.sql +32 -0
  12. package/migrations/0009_sso_states.sql +13 -0
  13. package/migrations/0010_notify_events.sql +23 -0
  14. package/migrations/0011_op_launch.sql +18 -0
  15. package/migrations/0011_org_memberships.sql +62 -0
  16. package/migrations/0012_notify_subscriptions.sql +57 -0
  17. package/migrations/0012_strong_auth.sql +109 -0
  18. package/migrations/0013_org_registry.sql +53 -0
  19. package/migrations/0014_notify_inbox.sql +34 -0
  20. package/migrations/0015_certificate_holder_attribution.sql +63 -0
  21. package/migrations/0016_instrument_registrations.sql +78 -0
  22. package/package.json +52 -0
  23. package/src/client-info.ts +25 -0
  24. package/src/context.ts +31 -0
  25. package/src/github.ts +284 -0
  26. package/src/mailer.ts +309 -0
  27. package/src/oidc.ts +369 -0
  28. package/src/profile/node.ts +83 -0
  29. package/src/profile.ts +582 -0
  30. package/src/rbac/node.ts +42 -0
  31. package/src/rbac.ts +53 -0
  32. package/src/session.ts +45 -0
  33. package/src/store/d1.ts +2850 -0
  34. package/src/store/sqlite/entities.ts +71 -0
  35. package/src/store/sqlite/events.ts +82 -0
  36. package/src/store/sqlite/factors-store.ts +348 -0
  37. package/src/store/sqlite/notify.ts +247 -0
  38. package/src/store/sqlite/op-accounts-store.ts +470 -0
  39. package/src/store/sqlite/op-store.ts +280 -0
  40. package/src/store/sqlite/schema.sql +745 -0
  41. package/src/store/sqlite/store.ts +1390 -0
  42. package/src/store/sqlite/upstream-store.ts +148 -0
  43. package/src/store/sqlite.ts +1027 -0
  44. package/src/store.ts +1826 -0
  45. package/src/vocab/index.ts +12 -0
  46. package/src/vocab/permissions.ts +398 -0
  47. package/src/vocab/rbac.ts +281 -0
  48. package/src/vocab/roles.ts +162 -0
@@ -0,0 +1,148 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // The upstream providers' SQLite half (TODO.identity/08) — the sync
3
+ // implementations behind the ServerStore upstream methods
4
+ // (sqlite-server-store.ts delegates here one-for-one, mirroring
5
+ // op-store.ts's role for the OP domain).
6
+ //
7
+ // Two tables:
8
+ // identity_providers — the upstream registry (GitHub + Google + Apple
9
+ // + Entra + generic OIDC; the client secret is NEVER stored, only
10
+ // the env reference);
11
+ // identity_links — the linked identities (TODO.identity/02's shape,
12
+ // landed additively here): THE match rule for an upstream sign-in
13
+ // is (provider, provider_account_id) — NEVER email alone.
14
+ //
15
+ // NODE-ONLY: better-sqlite3 through ./store's getDb. The Worker bundle
16
+ // never sees this module (the D1 store implements the same surface in
17
+ // d1-store.ts).
18
+ // ═══════════════════════════════════════════════════════════════════
19
+
20
+ import { randomUUID } from 'crypto'
21
+ import { getDb } from './store'
22
+ import type { IdentityLink, IdentityProvider } from '../../store'
23
+
24
+ function toIdentityProvider(row: Record<string, unknown>): IdentityProvider {
25
+ return {
26
+ id: row.id as string,
27
+ kind: row.kind as IdentityProvider['kind'],
28
+ displayName: row.display_name as string,
29
+ brandMark: (row.brand_mark as string | null) ?? null,
30
+ issuer: (row.issuer as string | null) ?? null,
31
+ clientId: row.client_id as string,
32
+ clientSecretRef: (row.client_secret_ref as string | null) ?? null,
33
+ scopes: (row.scopes as string | null) ?? null,
34
+ enabled: row.enabled === 1,
35
+ createdAt: row.created_at as string,
36
+ createdBy: (row.created_by as string | null) ?? null,
37
+ updatedAt: (row.updated_at as string | null) ?? null,
38
+ }
39
+ }
40
+
41
+ export function listIdentityProviders(): IdentityProvider[] {
42
+ const rows = getDb().prepare('SELECT * FROM identity_providers ORDER BY created_at, id').all() as Array<Record<string, unknown>>
43
+ return rows.map(toIdentityProvider)
44
+ }
45
+
46
+ export function getIdentityProvider(id: string): IdentityProvider | null {
47
+ const row = getDb().prepare('SELECT * FROM identity_providers WHERE id = ?').get(id) as Record<string, unknown> | undefined
48
+ return row ? toIdentityProvider(row) : null
49
+ }
50
+
51
+ export function upsertIdentityProvider(input: {
52
+ id: string
53
+ kind: IdentityProvider['kind']
54
+ displayName: string
55
+ brandMark?: string | null
56
+ issuer?: string | null
57
+ clientId: string
58
+ clientSecretRef?: string | null
59
+ scopes?: string | null
60
+ enabled?: boolean
61
+ createdBy?: string | null
62
+ }): IdentityProvider {
63
+ getDb().prepare(`
64
+ INSERT INTO identity_providers
65
+ (id, kind, display_name, brand_mark, issuer, client_id, client_secret_ref, scopes, enabled, created_by)
66
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
67
+ ON CONFLICT (id) DO UPDATE SET
68
+ kind = excluded.kind,
69
+ display_name = excluded.display_name,
70
+ brand_mark = excluded.brand_mark,
71
+ issuer = excluded.issuer,
72
+ client_id = excluded.client_id,
73
+ client_secret_ref = excluded.client_secret_ref,
74
+ scopes = excluded.scopes,
75
+ enabled = excluded.enabled,
76
+ updated_at = datetime('now')
77
+ `).run(
78
+ input.id,
79
+ input.kind,
80
+ input.displayName,
81
+ input.brandMark ?? null,
82
+ input.issuer ?? null,
83
+ input.clientId,
84
+ input.clientSecretRef ?? null,
85
+ input.scopes ?? null,
86
+ // Disabled by default (the schema's DEFAULT 0): a provider becomes
87
+ // visible to the login page only by a deliberate enable.
88
+ input.enabled ? 1 : 0,
89
+ input.createdBy ?? null,
90
+ )
91
+ return getIdentityProvider(input.id)!
92
+ }
93
+
94
+ export function setIdentityProviderEnabled(id: string, enabled: boolean): IdentityProvider | null {
95
+ const res = getDb().prepare("UPDATE identity_providers SET enabled = ?, updated_at = datetime('now') WHERE id = ?")
96
+ .run(enabled ? 1 : 0, id)
97
+ return res.changes > 0 ? getIdentityProvider(id) : null
98
+ }
99
+
100
+ export function deleteIdentityProvider(id: string): boolean {
101
+ const res = getDb().prepare('DELETE FROM identity_providers WHERE id = ?').run(id)
102
+ return res.changes > 0
103
+ }
104
+
105
+ // ── the linked identities ────────────────────────────────────────────
106
+
107
+ function toIdentityLink(row: Record<string, unknown>): IdentityLink {
108
+ return {
109
+ id: row.id as string,
110
+ userId: row.user_id as string,
111
+ provider: row.provider as string,
112
+ providerAccountId: row.provider_account_id as string,
113
+ linkedAt: row.linked_at as string,
114
+ linkedBy: (row.linked_by as string | null) ?? null,
115
+ }
116
+ }
117
+
118
+ export function listIdentityLinks(userId: string): IdentityLink[] {
119
+ const rows = getDb().prepare('SELECT * FROM identity_links WHERE user_id = ? ORDER BY linked_at, provider').all(userId) as Array<Record<string, unknown>>
120
+ return rows.map(toIdentityLink)
121
+ }
122
+
123
+ export function findIdentityLink(provider: string, providerAccountId: string): IdentityLink | null {
124
+ const row = getDb().prepare('SELECT * FROM identity_links WHERE provider = ? AND provider_account_id = ?')
125
+ .get(provider, providerAccountId) as Record<string, unknown> | undefined
126
+ return row ? toIdentityLink(row) : null
127
+ }
128
+
129
+ /** Create the link; NULL on the UNIQUE(provider, provider_account_id)
130
+ * conflict — the pair is already linked (to any account). */
131
+ export function createIdentityLink(input: {
132
+ userId: string
133
+ provider: string
134
+ providerAccountId: string
135
+ linkedBy?: string | null
136
+ }): IdentityLink | null {
137
+ const id = randomUUID()
138
+ const res = getDb().prepare(
139
+ 'INSERT OR IGNORE INTO identity_links (id, user_id, provider, provider_account_id, linked_by) VALUES (?, ?, ?, ?, ?)',
140
+ ).run(id, input.userId, input.provider, input.providerAccountId, input.linkedBy ?? null)
141
+ if (res.changes === 0) return null
142
+ return findIdentityLink(input.provider, input.providerAccountId)
143
+ }
144
+
145
+ export function deleteIdentityLink(userId: string, provider: string): boolean {
146
+ const res = getDb().prepare('DELETE FROM identity_links WHERE user_id = ? AND provider = ?').run(userId, provider)
147
+ return res.changes > 0
148
+ }