@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
|
@@ -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
|
+
}
|