@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,247 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // The notification subscriptions store (TODO.notify/02) — the SQLite
3
+ // (better-sqlite3, node-only) sync implementation, the events.ts
4
+ // pattern. Three tables:
5
+ //
6
+ // notify_rules the per-user rule rows (subscribe|mute on a
7
+ // key pattern, the pattern SPLIT into its pinned
8
+ // legs so the reverse match — every user's rules
9
+ // covering one event — resolves in SQL);
10
+ // notify_entity_mutes the thread-level mutes (the bell's Muted
11
+ // state, the email footer's unsubscribe);
12
+ // notify_preferences one row per user: the per-category email
13
+ // posture (immediate / digest / off).
14
+ //
15
+ // Plus TODO.notify/03's inbox state: notify_inbox_state, the per-user
16
+ // per-event read/done markers the computed feed joins (one row per
17
+ // (user, event), written lazily at the first act on the row).
18
+ //
19
+ // The D1 store (../d1.ts) runs the SAME statements against the binding;
20
+ // the d1-store suite's tripwire pins the two schemas in lockstep.
21
+ // ═══════════════════════════════════════════════════════════════════
22
+
23
+ import { getDb } from './store'
24
+ import type {
25
+ NotifyEntityMute,
26
+ NotifyInboxState,
27
+ NotifyPreferences,
28
+ NotifyRule,
29
+ } from '../../store'
30
+
31
+ interface RuleRow {
32
+ id: string
33
+ user_id: string
34
+ pattern: string
35
+ domain: string
36
+ entity_id: string | null
37
+ action: string | null
38
+ mode: string
39
+ channel_overrides: string | null
40
+ created_at: string
41
+ }
42
+
43
+ function toNotifyRule(row: RuleRow): NotifyRule {
44
+ return {
45
+ id: row.id,
46
+ userId: row.user_id,
47
+ pattern: row.pattern,
48
+ domain: row.domain,
49
+ entityId: row.entity_id,
50
+ action: row.action,
51
+ mode: row.mode as NotifyRule['mode'],
52
+ channelOverrides: row.channel_overrides,
53
+ createdAt: row.created_at,
54
+ }
55
+ }
56
+
57
+ export function listNotifyRules(userId: string): NotifyRule[] {
58
+ const rows = getDb()
59
+ .prepare('SELECT * FROM notify_rules WHERE user_id = ? ORDER BY created_at, id')
60
+ .all(userId) as RuleRow[]
61
+ return rows.map(toNotifyRule)
62
+ }
63
+
64
+ export function putNotifyRule(input: {
65
+ id: string
66
+ userId: string
67
+ pattern: string
68
+ domain: string
69
+ entityId: string | null
70
+ action: string | null
71
+ mode: NotifyRule['mode']
72
+ channelOverrides: string | null
73
+ }): NotifyRule {
74
+ const db = getDb()
75
+ db.prepare(
76
+ `INSERT INTO notify_rules (id, user_id, pattern, domain, entity_id, action, mode, channel_overrides)
77
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
78
+ ON CONFLICT (user_id, pattern) DO UPDATE SET
79
+ domain = excluded.domain,
80
+ entity_id = excluded.entity_id,
81
+ action = excluded.action,
82
+ mode = excluded.mode,
83
+ channel_overrides = excluded.channel_overrides`,
84
+ ).run(input.id, input.userId, input.pattern, input.domain, input.entityId, input.action, input.mode, input.channelOverrides)
85
+ return toNotifyRule(
86
+ db.prepare('SELECT * FROM notify_rules WHERE user_id = ? AND pattern = ?').get(input.userId, input.pattern) as RuleRow,
87
+ )
88
+ }
89
+
90
+ export function deleteNotifyRule(userId: string, pattern: string): boolean {
91
+ const res = getDb()
92
+ .prepare('DELETE FROM notify_rules WHERE user_id = ? AND pattern = ?')
93
+ .run(userId, pattern)
94
+ return res.changes > 0
95
+ }
96
+
97
+ /** The resolution's reverse match: a rule covers the event when its
98
+ * pinned legs equal the event's columns (NULL = the wild leg). */
99
+ export function notifyRulesForEvent(filter: { domain: string; entityId: string; action: string }): NotifyRule[] {
100
+ const rows = getDb()
101
+ .prepare(
102
+ `SELECT * FROM notify_rules
103
+ WHERE domain = ? AND (entity_id IS NULL OR entity_id = ?) AND (action IS NULL OR action = ?)
104
+ ORDER BY created_at, id`,
105
+ )
106
+ .all(filter.domain, filter.entityId, filter.action) as RuleRow[]
107
+ return rows.map(toNotifyRule)
108
+ }
109
+
110
+ interface EntityMuteRow {
111
+ id: string
112
+ user_id: string
113
+ domain: string
114
+ entity_id: string
115
+ created_at: string
116
+ }
117
+
118
+ function toNotifyEntityMute(row: EntityMuteRow): NotifyEntityMute {
119
+ return {
120
+ id: row.id,
121
+ userId: row.user_id,
122
+ domain: row.domain,
123
+ entityId: row.entity_id,
124
+ createdAt: row.created_at,
125
+ }
126
+ }
127
+
128
+ export function listNotifyEntityMutes(userId: string): NotifyEntityMute[] {
129
+ const rows = getDb()
130
+ .prepare('SELECT * FROM notify_entity_mutes WHERE user_id = ? ORDER BY created_at, id')
131
+ .all(userId) as EntityMuteRow[]
132
+ return rows.map(toNotifyEntityMute)
133
+ }
134
+
135
+ export function putNotifyEntityMute(input: {
136
+ id: string
137
+ userId: string
138
+ domain: string
139
+ entityId: string
140
+ }): NotifyEntityMute {
141
+ const db = getDb()
142
+ db.prepare(
143
+ `INSERT INTO notify_entity_mutes (id, user_id, domain, entity_id) VALUES (?, ?, ?, ?)
144
+ ON CONFLICT (user_id, domain, entity_id) DO NOTHING`,
145
+ ).run(input.id, input.userId, input.domain, input.entityId)
146
+ return toNotifyEntityMute(
147
+ db.prepare(
148
+ 'SELECT * FROM notify_entity_mutes WHERE user_id = ? AND domain = ? AND entity_id = ?',
149
+ ).get(input.userId, input.domain, input.entityId) as EntityMuteRow,
150
+ )
151
+ }
152
+
153
+ export function deleteNotifyEntityMute(userId: string, domain: string, entityId: string): boolean {
154
+ const res = getDb()
155
+ .prepare('DELETE FROM notify_entity_mutes WHERE user_id = ? AND domain = ? AND entity_id = ?')
156
+ .run(userId, domain, entityId)
157
+ return res.changes > 0
158
+ }
159
+
160
+ export function notifyEntityMutesForEvent(domain: string, entityId: string): NotifyEntityMute[] {
161
+ const rows = getDb()
162
+ .prepare('SELECT * FROM notify_entity_mutes WHERE domain = ? AND entity_id = ? ORDER BY created_at, id')
163
+ .all(domain, entityId) as EntityMuteRow[]
164
+ return rows.map(toNotifyEntityMute)
165
+ }
166
+
167
+ interface PreferencesRow {
168
+ user_id: string
169
+ channels: string
170
+ updated_at: string
171
+ }
172
+
173
+ function toNotifyPreferences(row: PreferencesRow): NotifyPreferences {
174
+ return { userId: row.user_id, channels: row.channels, updatedAt: row.updated_at }
175
+ }
176
+
177
+ export function getNotifyPreferences(userId: string): NotifyPreferences | null {
178
+ const row = getDb()
179
+ .prepare('SELECT * FROM notify_preferences WHERE user_id = ?')
180
+ .get(userId) as PreferencesRow | undefined
181
+ return row ? toNotifyPreferences(row) : null
182
+ }
183
+
184
+ export function putNotifyPreferences(userId: string, channels: string): NotifyPreferences {
185
+ const db = getDb()
186
+ db.prepare(
187
+ `INSERT INTO notify_preferences (user_id, channels) VALUES (?, ?)
188
+ ON CONFLICT (user_id) DO UPDATE SET channels = excluded.channels, updated_at = datetime('now')`,
189
+ ).run(userId, channels)
190
+ return toNotifyPreferences(
191
+ db.prepare('SELECT * FROM notify_preferences WHERE user_id = ?').get(userId) as PreferencesRow,
192
+ )
193
+ }
194
+
195
+ // ── the inbox state (TODO.notify/03) ─────────────────────────────────
196
+
197
+ interface InboxStateRow {
198
+ user_id: string
199
+ event_id: string
200
+ read_at: string | null
201
+ done_at: string | null
202
+ created_at: string
203
+ }
204
+
205
+ function toNotifyInboxState(row: InboxStateRow): NotifyInboxState {
206
+ return {
207
+ userId: row.user_id,
208
+ eventId: row.event_id,
209
+ readAt: row.read_at,
210
+ doneAt: row.done_at,
211
+ createdAt: row.created_at,
212
+ }
213
+ }
214
+
215
+ export function listNotifyInboxStates(userId: string): NotifyInboxState[] {
216
+ const rows = getDb()
217
+ .prepare('SELECT * FROM notify_inbox_state WHERE user_id = ? ORDER BY created_at, event_id')
218
+ .all(userId) as InboxStateRow[]
219
+ return rows.map(toNotifyInboxState)
220
+ }
221
+
222
+ /** The marker write: each PRESENT flag stamps (datetime('now')) or
223
+ * clears (NULL) its column; absent flags keep. The row lands on the
224
+ * first act (INSERT with both columns defaulted), later acts UPDATE. */
225
+ export function putNotifyInboxState(input: {
226
+ userId: string
227
+ eventId: string
228
+ read?: boolean
229
+ done?: boolean
230
+ }): NotifyInboxState {
231
+ const db = getDb()
232
+ db.prepare(
233
+ 'INSERT OR IGNORE INTO notify_inbox_state (user_id, event_id) VALUES (?, ?)',
234
+ ).run(input.userId, input.eventId)
235
+ if (input.read !== undefined) {
236
+ db.prepare("UPDATE notify_inbox_state SET read_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE user_id = ? AND event_id = ?")
237
+ .run(input.read ? 1 : 0, input.userId, input.eventId)
238
+ }
239
+ if (input.done !== undefined) {
240
+ db.prepare("UPDATE notify_inbox_state SET done_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE user_id = ? AND event_id = ?")
241
+ .run(input.done ? 1 : 0, input.userId, input.eventId)
242
+ }
243
+ return toNotifyInboxState(
244
+ db.prepare('SELECT * FROM notify_inbox_state WHERE user_id = ? AND event_id = ?')
245
+ .get(input.userId, input.eventId) as InboxStateRow,
246
+ )
247
+ }