@indecisivedev/openplan 0.2.0-alpha.2

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/access-policy.d.ts +35 -0
  4. package/dist/access-policy.js +107 -0
  5. package/dist/access-policy.js.map +1 -0
  6. package/dist/access-store.d.ts +33 -0
  7. package/dist/access-store.js +212 -0
  8. package/dist/access-store.js.map +1 -0
  9. package/dist/admin.d.ts +1 -0
  10. package/dist/admin.js +13 -0
  11. package/dist/admin.js.map +1 -0
  12. package/dist/api.d.ts +2 -0
  13. package/dist/api.js +867 -0
  14. package/dist/api.js.map +1 -0
  15. package/dist/bootstrap-admin.d.ts +1 -0
  16. package/dist/bootstrap-admin.js +35 -0
  17. package/dist/bootstrap-admin.js.map +1 -0
  18. package/dist/browser-security.d.ts +5 -0
  19. package/dist/browser-security.js +53 -0
  20. package/dist/browser-security.js.map +1 -0
  21. package/dist/celld-quickstart.d.ts +51 -0
  22. package/dist/celld-quickstart.js +1175 -0
  23. package/dist/celld-quickstart.js.map +1 -0
  24. package/dist/cli-api-client.d.ts +78 -0
  25. package/dist/cli-api-client.js +272 -0
  26. package/dist/cli-api-client.js.map +1 -0
  27. package/dist/cli-credentials.d.ts +47 -0
  28. package/dist/cli-credentials.js +447 -0
  29. package/dist/cli-credentials.js.map +1 -0
  30. package/dist/cli-endpoint.d.ts +19 -0
  31. package/dist/cli-endpoint.js +56 -0
  32. package/dist/cli-endpoint.js.map +1 -0
  33. package/dist/cli-program.d.ts +11 -0
  34. package/dist/cli-program.js +934 -0
  35. package/dist/cli-program.js.map +1 -0
  36. package/dist/cli.d.ts +2 -0
  37. package/dist/cli.js +7 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/client-ip.d.ts +4 -0
  40. package/dist/client-ip.js +49 -0
  41. package/dist/client-ip.js.map +1 -0
  42. package/dist/config.d.ts +35 -0
  43. package/dist/config.js +113 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/crypto.d.ts +3 -0
  46. package/dist/crypto.js +11 -0
  47. package/dist/crypto.js.map +1 -0
  48. package/dist/database.d.ts +15 -0
  49. package/dist/database.js +155 -0
  50. package/dist/database.js.map +1 -0
  51. package/dist/db.d.ts +31 -0
  52. package/dist/db.js +388 -0
  53. package/dist/db.js.map +1 -0
  54. package/dist/drafts.d.ts +43 -0
  55. package/dist/drafts.js +90 -0
  56. package/dist/drafts.js.map +1 -0
  57. package/dist/feature-settings.d.ts +7 -0
  58. package/dist/feature-settings.js +32 -0
  59. package/dist/feature-settings.js.map +1 -0
  60. package/dist/generated/annotation-client.d.ts +1 -0
  61. package/dist/generated/annotation-client.js +2 -0
  62. package/dist/generated/annotation-client.js.map +1 -0
  63. package/dist/html-policy.d.ts +15 -0
  64. package/dist/html-policy.js +172 -0
  65. package/dist/html-policy.js.map +1 -0
  66. package/dist/ids.d.ts +2 -0
  67. package/dist/ids.js +10 -0
  68. package/dist/ids.js.map +1 -0
  69. package/dist/local-auth.d.ts +36 -0
  70. package/dist/local-auth.js +215 -0
  71. package/dist/local-auth.js.map +1 -0
  72. package/dist/maintenance.d.ts +1 -0
  73. package/dist/maintenance.js +18 -0
  74. package/dist/maintenance.js.map +1 -0
  75. package/dist/oidc.d.ts +39 -0
  76. package/dist/oidc.js +194 -0
  77. package/dist/oidc.js.map +1 -0
  78. package/dist/public-url.d.ts +17 -0
  79. package/dist/public-url.js +96 -0
  80. package/dist/public-url.js.map +1 -0
  81. package/dist/rate-limit.d.ts +25 -0
  82. package/dist/rate-limit.js +55 -0
  83. package/dist/rate-limit.js.map +1 -0
  84. package/dist/reconcile.d.ts +5 -0
  85. package/dist/reconcile.js +18 -0
  86. package/dist/reconcile.js.map +1 -0
  87. package/dist/render-annotations.d.ts +10 -0
  88. package/dist/render-annotations.js +73 -0
  89. package/dist/render-annotations.js.map +1 -0
  90. package/dist/render-web.d.ts +74 -0
  91. package/dist/render-web.js +439 -0
  92. package/dist/render-web.js.map +1 -0
  93. package/dist/render.d.ts +4 -0
  94. package/dist/render.js +85 -0
  95. package/dist/render.js.map +1 -0
  96. package/dist/server.d.ts +1 -0
  97. package/dist/server.js +26 -0
  98. package/dist/server.js.map +1 -0
  99. package/dist/shoo.d.ts +26 -0
  100. package/dist/shoo.js +92 -0
  101. package/dist/shoo.js.map +1 -0
  102. package/dist/sqlite-migrations.d.ts +6 -0
  103. package/dist/sqlite-migrations.js +214 -0
  104. package/dist/sqlite-migrations.js.map +1 -0
  105. package/dist/storage.d.ts +14 -0
  106. package/dist/storage.js +86 -0
  107. package/dist/storage.js.map +1 -0
  108. package/dist/types.d.ts +24 -0
  109. package/dist/types.js +2 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/upload-state.d.ts +19 -0
  112. package/dist/upload-state.js +118 -0
  113. package/dist/upload-state.js.map +1 -0
  114. package/dist/web-auth.d.ts +33 -0
  115. package/dist/web-auth.js +122 -0
  116. package/dist/web-auth.js.map +1 -0
  117. package/dist/web.d.ts +2 -0
  118. package/dist/web.js +550 -0
  119. package/dist/web.js.map +1 -0
  120. package/package.json +70 -0
package/dist/db.js ADDED
@@ -0,0 +1,388 @@
1
+ import { config } from "./config.js";
2
+ import { sha256 } from "./crypto.js";
3
+ import { database } from "./database.js";
4
+ import { newInternalId } from "./ids.js";
5
+ export const publicUploadAuth = {
6
+ keyHash: "",
7
+ id: "key_public_upload",
8
+ account_id: "acct_public_upload",
9
+ name: "Public Uploads",
10
+ account_name: "Public Uploads"
11
+ };
12
+ export const pool = database;
13
+ export async function initDb() {
14
+ await database.init();
15
+ if (database.kind === "sqlite") {
16
+ await ensurePublicUploadApiKey();
17
+ return;
18
+ }
19
+ await pool.query(`
20
+ CREATE TABLE IF NOT EXISTS accounts (
21
+ id TEXT PRIMARY KEY,
22
+ name TEXT NOT NULL,
23
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
24
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS draft_access_policies (
28
+ id TEXT PRIMARY KEY,
29
+ mode TEXT NOT NULL CHECK (mode IN ('private', 'password', 'public')),
30
+ password_algorithm TEXT,
31
+ password_iterations INTEGER,
32
+ password_salt TEXT,
33
+ password_hash TEXT,
34
+ credential_revision INTEGER NOT NULL DEFAULT 1,
35
+ created_by_account_id TEXT NOT NULL REFERENCES accounts(id),
36
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
37
+ CHECK (
38
+ (mode = 'password' AND password_algorithm IS NOT NULL AND password_iterations IS NOT NULL
39
+ AND password_salt IS NOT NULL AND password_hash IS NOT NULL)
40
+ OR
41
+ (mode <> 'password' AND password_algorithm IS NULL AND password_iterations IS NULL
42
+ AND password_salt IS NULL AND password_hash IS NULL)
43
+ )
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS api_keys (
47
+ id TEXT PRIMARY KEY,
48
+ account_id TEXT NOT NULL REFERENCES accounts(id),
49
+ name TEXT NOT NULL,
50
+ key_hash TEXT NOT NULL UNIQUE,
51
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
52
+ last_used_at TIMESTAMPTZ,
53
+ revoked_at TIMESTAMPTZ
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS drafts (
57
+ id TEXT PRIMARY KEY,
58
+ account_id TEXT NOT NULL REFERENCES accounts(id),
59
+ title TEXT NOT NULL,
60
+ description TEXT,
61
+ current_version_id TEXT,
62
+ repo_org TEXT,
63
+ repo_name TEXT,
64
+ repo_host TEXT,
65
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
66
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
67
+ deleted_at TIMESTAMPTZ,
68
+ disabled_at TIMESTAMPTZ,
69
+ disabled_reason TEXT,
70
+ default_access_policy_id TEXT REFERENCES draft_access_policies(id)
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS draft_versions (
74
+ id TEXT PRIMARY KEY,
75
+ draft_id TEXT NOT NULL REFERENCES drafts(id),
76
+ version_number INTEGER NOT NULL,
77
+ object_key TEXT NOT NULL,
78
+ content_hash TEXT NOT NULL,
79
+ file_size INTEGER NOT NULL,
80
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
81
+ created_by_api_key_id TEXT NOT NULL REFERENCES api_keys(id),
82
+ source_ip TEXT,
83
+ user_agent TEXT,
84
+ cli_version TEXT,
85
+ git_branch TEXT,
86
+ git_commit_sha TEXT,
87
+ git_commit_subject TEXT,
88
+ git_dirty BOOLEAN,
89
+ original_filename TEXT,
90
+ request_id TEXT,
91
+ has_inline_script BOOLEAN,
92
+ external_image_hosts JSONB,
93
+ ci_run_url TEXT,
94
+ ci_actor TEXT,
95
+ access_policy_id TEXT REFERENCES draft_access_policies(id),
96
+ UNIQUE (draft_id, version_number)
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS upload_events (
100
+ id TEXT PRIMARY KEY,
101
+ draft_id TEXT NOT NULL REFERENCES drafts(id),
102
+ draft_version_id TEXT REFERENCES draft_versions(id),
103
+ api_key_id TEXT NOT NULL REFERENCES api_keys(id),
104
+ event_type TEXT NOT NULL,
105
+ source_ip TEXT,
106
+ user_agent TEXT,
107
+ metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
108
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS identities (
112
+ id TEXT PRIMARY KEY,
113
+ account_id TEXT NOT NULL REFERENCES accounts(id),
114
+ provider TEXT NOT NULL,
115
+ subject TEXT NOT NULL,
116
+ email TEXT,
117
+ email_verified BOOLEAN,
118
+ display_name TEXT,
119
+ picture_url TEXT,
120
+ pii_subject TEXT,
121
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
122
+ last_login_at TIMESTAMPTZ,
123
+ UNIQUE (provider, subject)
124
+ );
125
+
126
+ CREATE TABLE IF NOT EXISTS account_access_grants (
127
+ viewer_account_id TEXT NOT NULL REFERENCES accounts(id),
128
+ resource_account_id TEXT NOT NULL REFERENCES accounts(id),
129
+ created_by_api_key_id TEXT NOT NULL REFERENCES api_keys(id),
130
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
131
+ PRIMARY KEY (viewer_account_id, resource_account_id),
132
+ CHECK (viewer_account_id <> resource_account_id)
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS access_policy_events (
136
+ id TEXT PRIMARY KEY,
137
+ draft_id TEXT NOT NULL REFERENCES drafts(id),
138
+ draft_version_id TEXT REFERENCES draft_versions(id),
139
+ actor_account_id TEXT NOT NULL REFERENCES accounts(id),
140
+ actor_api_key_id TEXT REFERENCES api_keys(id),
141
+ previous_mode TEXT,
142
+ new_mode TEXT NOT NULL,
143
+ policy_id TEXT REFERENCES draft_access_policies(id),
144
+ credential_revision INTEGER,
145
+ request_id TEXT,
146
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
147
+ );
148
+
149
+ CREATE TABLE IF NOT EXISTS site_settings (
150
+ key TEXT PRIMARY KEY,
151
+ enabled BOOLEAN NOT NULL,
152
+ updated_by_account_id TEXT NOT NULL REFERENCES accounts(id),
153
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
154
+ );
155
+
156
+ CREATE TABLE IF NOT EXISTS admin_events (
157
+ id TEXT PRIMARY KEY,
158
+ actor_account_id TEXT NOT NULL REFERENCES accounts(id),
159
+ event_type TEXT NOT NULL,
160
+ setting_key TEXT NOT NULL,
161
+ previous_enabled BOOLEAN,
162
+ new_enabled BOOLEAN NOT NULL,
163
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
164
+ );
165
+
166
+ CREATE INDEX IF NOT EXISTS draft_versions_draft_id_idx ON draft_versions(draft_id);
167
+ CREATE INDEX IF NOT EXISTS upload_events_draft_id_idx ON upload_events(draft_id);
168
+ CREATE INDEX IF NOT EXISTS drafts_account_id_idx ON drafts(account_id);
169
+ CREATE INDEX IF NOT EXISTS account_access_grants_resource_idx ON account_access_grants(resource_account_id, viewer_account_id);
170
+ CREATE INDEX IF NOT EXISTS access_policy_events_draft_idx ON access_policy_events(draft_id, created_at DESC);
171
+ CREATE INDEX IF NOT EXISTS admin_events_setting_idx ON admin_events(setting_key, created_at DESC);
172
+
173
+ -- Backfill columns for databases created before they were introduced.
174
+ ALTER TABLE identities ADD COLUMN IF NOT EXISTS email TEXT;
175
+ ALTER TABLE identities ADD COLUMN IF NOT EXISTS email_verified BOOLEAN;
176
+ ALTER TABLE identities ADD COLUMN IF NOT EXISTS display_name TEXT;
177
+ ALTER TABLE identities ADD COLUMN IF NOT EXISTS picture_url TEXT;
178
+ ALTER TABLE identities ADD COLUMN IF NOT EXISTS pii_subject TEXT;
179
+ ALTER TABLE drafts ADD COLUMN IF NOT EXISTS description TEXT;
180
+ ALTER TABLE drafts ADD COLUMN IF NOT EXISTS repo_host TEXT;
181
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS git_commit_subject TEXT;
182
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS git_dirty BOOLEAN;
183
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS request_id TEXT;
184
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS has_inline_script BOOLEAN;
185
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS external_image_hosts JSONB;
186
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS ci_run_url TEXT;
187
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS ci_actor TEXT;
188
+ ALTER TABLE drafts ADD COLUMN IF NOT EXISTS default_access_policy_id TEXT REFERENCES draft_access_policies(id);
189
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS access_policy_id TEXT REFERENCES draft_access_policies(id);
190
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'ready';
191
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS failure_reason TEXT;
192
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS finalized_at TIMESTAMPTZ;
193
+ ALTER TABLE draft_versions ADD COLUMN IF NOT EXISTS draft_metadata_json JSONB;
194
+ ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS parent_api_key_id TEXT REFERENCES api_keys(id);
195
+ CREATE INDEX IF NOT EXISTS api_keys_parent_idx ON api_keys(parent_api_key_id);
196
+ CREATE INDEX IF NOT EXISTS draft_versions_status_idx ON draft_versions(status, created_at);
197
+ `);
198
+ await ensurePublicUploadApiKey();
199
+ }
200
+ export async function ensureBootstrapApiKey() {
201
+ if (!config.bootstrapApiKey)
202
+ return;
203
+ const accountId = "acct_bootstrap";
204
+ const apiKeyId = "key_bootstrap";
205
+ const keyHash = sha256(config.bootstrapApiKey);
206
+ await withTransaction(async (client) => {
207
+ // The account upsert locks the same row used by API mint/revoke operations.
208
+ await client.query(`
209
+ INSERT INTO accounts (id, name) VALUES ($1, $2)
210
+ ON CONFLICT (id) DO UPDATE SET updated_at = now()
211
+ `, [accountId, "Bootstrap Account"]);
212
+ const previous = await client.query("SELECT key_hash FROM api_keys WHERE id = $1", [apiKeyId]);
213
+ if (previous.rows[0] && previous.rows[0].key_hash !== keyHash) {
214
+ const descendants = `WITH RECURSIVE descendants(id) AS (
215
+ SELECT id FROM api_keys WHERE id = $1 AND account_id = $2
216
+ UNION
217
+ SELECT k.id FROM api_keys k JOIN descendants d ON k.parent_api_key_id = d.id
218
+ WHERE k.account_id = $2
219
+ )`;
220
+ // Remove grants before restoring the root so prior root grants cannot revive.
221
+ await client.query(`${descendants}
222
+ DELETE FROM account_access_grants
223
+ WHERE created_by_api_key_id IN (SELECT id FROM descendants)
224
+ RETURNING created_by_api_key_id
225
+ `, [apiKeyId, accountId]);
226
+ await client.query(`${descendants}
227
+ UPDATE api_keys SET revoked_at = now()
228
+ WHERE id IN (SELECT id FROM descendants) AND revoked_at IS NULL
229
+ RETURNING id
230
+ `, [apiKeyId, accountId]);
231
+ }
232
+ await client.query(`
233
+ INSERT INTO api_keys (id, account_id, name, key_hash)
234
+ VALUES ($1, $2, $3, $4)
235
+ ON CONFLICT (id) DO UPDATE SET
236
+ key_hash = EXCLUDED.key_hash,
237
+ name = EXCLUDED.name,
238
+ revoked_at = CASE WHEN api_keys.key_hash <> EXCLUDED.key_hash
239
+ THEN NULL ELSE api_keys.revoked_at END
240
+ `, [apiKeyId, accountId, "Bootstrap API Key", keyHash]);
241
+ });
242
+ }
243
+ export async function findApiKeyByToken(token) {
244
+ const keyHash = sha256(token);
245
+ const result = await pool.query(`
246
+ SELECT api_keys.id, api_keys.account_id, api_keys.name, accounts.name AS account_name
247
+ FROM api_keys
248
+ JOIN accounts ON accounts.id = api_keys.account_id
249
+ WHERE api_keys.key_hash = $1
250
+ AND api_keys.id <> $2
251
+ AND api_keys.revoked_at IS NULL
252
+ LIMIT 1
253
+ `, [keyHash, publicUploadAuth.id]);
254
+ const apiKey = result.rows[0] || null;
255
+ if (apiKey) {
256
+ Object.defineProperty(apiKey, "keyHash", { value: keyHash, enumerable: false });
257
+ await pool.query("UPDATE api_keys SET last_used_at = now() WHERE id = $1", [apiKey.id]);
258
+ }
259
+ return apiKey;
260
+ }
261
+ async function ensurePublicUploadApiKey() {
262
+ await pool.query(`
263
+ INSERT INTO accounts (id, name)
264
+ VALUES ($1, $2)
265
+ ON CONFLICT (id) DO UPDATE
266
+ SET name = EXCLUDED.name,
267
+ updated_at = now()
268
+ `, [publicUploadAuth.account_id, publicUploadAuth.account_name]);
269
+ await pool.query(`
270
+ INSERT INTO api_keys (id, account_id, name, key_hash)
271
+ VALUES ($1, $2, $3, $4)
272
+ ON CONFLICT (id) DO UPDATE
273
+ SET key_hash = EXCLUDED.key_hash,
274
+ name = EXCLUDED.name,
275
+ revoked_at = NULL
276
+ `, [
277
+ publicUploadAuth.id,
278
+ publicUploadAuth.account_id,
279
+ publicUploadAuth.name,
280
+ sha256("openplan-public-upload-sentinel")
281
+ ]);
282
+ }
283
+ export async function withTransaction(work) {
284
+ return database.transaction(work);
285
+ }
286
+ export function newEventId() {
287
+ return newInternalId();
288
+ }
289
+ export async function findOrCreateAccountForIdentity({ provider, subject, profile = {} }) {
290
+ try {
291
+ return await upsertIdentity({ provider, subject, profile });
292
+ }
293
+ catch (error) {
294
+ // Two concurrent first sign-ins (e.g. two devices) can both miss the
295
+ // SELECT and collide on UNIQUE(provider, subject). The loser's transaction
296
+ // rolled back entirely (including its orphan account), so one retry hits
297
+ // the existing-identity path and succeeds.
298
+ if (isPgError(error) && error.code === "23505") {
299
+ return upsertIdentity({ provider, subject, profile });
300
+ }
301
+ throw error;
302
+ }
303
+ }
304
+ async function upsertIdentity({ provider, subject, profile = {} }) {
305
+ const profileParams = [
306
+ profile.email ?? null,
307
+ profile.emailVerified ?? null,
308
+ profile.displayName ?? null,
309
+ profile.pictureUrl ?? null,
310
+ profile.piiSubject ?? null
311
+ ];
312
+ return withTransaction(async (client) => {
313
+ const existing = await client.query(`
314
+ SELECT identities.account_id, accounts.name AS account_name
315
+ FROM identities
316
+ JOIN accounts ON accounts.id = identities.account_id
317
+ WHERE identities.provider = $1 AND identities.subject = $2
318
+ LIMIT 1
319
+ `, [provider, subject]);
320
+ if (existing.rows[0]) {
321
+ const accountId = existing.rows[0].account_id;
322
+ // The verified token is authoritative for profile fields: a claim absent
323
+ // from this login (e.g. the user removed their Google picture) clears the
324
+ // stored value, so DB, session, and header always agree. pii_subject is
325
+ // the one exception — it is a stable identifier, not editable profile,
326
+ // and a transient absence must not unlink the account.
327
+ await client.query(`
328
+ UPDATE identities
329
+ SET last_login_at = now(),
330
+ email = $3,
331
+ email_verified = $4,
332
+ display_name = $5,
333
+ picture_url = $6,
334
+ pii_subject = COALESCE($7, pii_subject)
335
+ WHERE provider = $1 AND subject = $2
336
+ `, [provider, subject, ...profileParams]);
337
+ // Same authoritative-token rule for the derived account name: if the
338
+ // user clears their Google name/email, the account label degrades to the
339
+ // neutral fallback instead of retaining old PII.
340
+ const accountName = profile.displayName || profile.email || `Openplan ${subject.slice(-6)}`;
341
+ if (accountName !== existing.rows[0].account_name) {
342
+ await client.query("UPDATE accounts SET name = $2, updated_at = now() WHERE id = $1", [
343
+ accountId,
344
+ accountName
345
+ ]);
346
+ }
347
+ return { accountId, accountName, email: profile.email ?? null, pictureUrl: profile.pictureUrl ?? null };
348
+ }
349
+ const accountId = `acct_${newInternalId()}`;
350
+ const accountName = profile.displayName || profile.email || `Openplan ${subject.slice(-6)}`;
351
+ await client.query("INSERT INTO accounts (id, name) VALUES ($1, $2)", [
352
+ accountId,
353
+ accountName
354
+ ]);
355
+ await client.query(`
356
+ INSERT INTO identities (
357
+ id, account_id, provider, subject,
358
+ email, email_verified, display_name, picture_url, pii_subject,
359
+ last_login_at
360
+ )
361
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
362
+ `, [newInternalId(), accountId, provider, subject, ...profileParams]);
363
+ return { accountId, accountName, email: profile.email ?? null, pictureUrl: profile.pictureUrl ?? null };
364
+ });
365
+ }
366
+ function isPgError(error) {
367
+ return error instanceof Error && "code" in error && typeof error.code === "string";
368
+ }
369
+ /** Revoking a key also invalidates every key minted through it. */
370
+ export async function revokeApiKey(accountId, apiKeyId) {
371
+ // Serialize account-level mint/revoke operations, including concurrent siblings.
372
+ return withTransaction(async (client) => {
373
+ await client.query("SELECT id FROM accounts WHERE id = $1 FOR UPDATE", [accountId]);
374
+ const result = await client.query(`
375
+ WITH RECURSIVE descendants(id) AS (
376
+ SELECT id FROM api_keys WHERE id = $1 AND account_id = $2
377
+ UNION
378
+ SELECT k.id FROM api_keys k JOIN descendants d ON k.parent_api_key_id = d.id
379
+ WHERE k.account_id = $2
380
+ )
381
+ UPDATE api_keys SET revoked_at = now()
382
+ WHERE id IN (SELECT id FROM descendants) AND revoked_at IS NULL
383
+ RETURNING id
384
+ `, [apiKeyId, accountId]);
385
+ return result.rowCount > 0;
386
+ });
387
+ }
388
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { QueryRow } from "./database.js";
2
+ export type DateValue = Date | string;
3
+ export interface DraftSummary {
4
+ draftId: string;
5
+ title: string;
6
+ description: string | null;
7
+ repoOrg: string | null;
8
+ repoName: string | null;
9
+ repoHost: string | null;
10
+ latestVersionNumber: number | null;
11
+ versionCount: number;
12
+ createdAt: DateValue;
13
+ updatedAt: DateValue;
14
+ latestVersionAt: DateValue | null;
15
+ disabled: boolean;
16
+ publicUrl: string;
17
+ rawUrl: string;
18
+ }
19
+ export interface DraftVersion extends QueryRow {
20
+ id: string;
21
+ version_number: number;
22
+ created_at: DateValue;
23
+ git_branch: string | null;
24
+ git_commit_sha: string | null;
25
+ git_commit_subject: string | null;
26
+ git_dirty: boolean | null;
27
+ file_size: number;
28
+ }
29
+ export interface DraftDetailResult {
30
+ draft: {
31
+ draftId: string;
32
+ title: string;
33
+ description: string | null;
34
+ publicUrl: string;
35
+ };
36
+ versions: DraftVersion[];
37
+ }
38
+ export declare function listAccountDrafts(accountId: string, { requestBaseUrl }: {
39
+ requestBaseUrl: string;
40
+ }): Promise<DraftSummary[]>;
41
+ export declare function getAccountDraftWithVersions(accountId: string, draftId: string, { requestBaseUrl }: {
42
+ requestBaseUrl: string;
43
+ }): Promise<DraftDetailResult | null>;
package/dist/drafts.js ADDED
@@ -0,0 +1,90 @@
1
+ import { pool } from "./db.js";
2
+ import { config } from "./config.js";
3
+ import { getDraftPublicUrl, getDraftRawUrl } from "./public-url.js";
4
+ // The "my docs" feed: every draft owned by an account, newest first, with the
5
+ // aggregates a dashboard needs (latest version, version count, repo). Shared
6
+ // by GET /api/drafts and the server-rendered dashboard.
7
+ export async function listAccountDrafts(accountId, { requestBaseUrl }) {
8
+ const result = await pool.query(`
9
+ SELECT
10
+ d.id,
11
+ d.title,
12
+ d.description,
13
+ d.repo_org,
14
+ d.repo_name,
15
+ d.repo_host,
16
+ d.created_at,
17
+ d.updated_at,
18
+ d.disabled_at,
19
+ cv.version_number AS latest_version_number,
20
+ cv.created_at AS latest_version_at,
21
+ COALESCE(vc.version_count, 0) AS version_count
22
+ FROM drafts d
23
+ LEFT JOIN draft_versions cv ON cv.id = d.current_version_id
24
+ LEFT JOIN (
25
+ SELECT draft_id, COUNT(*)::int AS version_count
26
+ FROM draft_versions
27
+ WHERE status = 'ready'
28
+ GROUP BY draft_id
29
+ ) vc ON vc.draft_id = d.id
30
+ WHERE d.account_id = $1
31
+ AND d.deleted_at IS NULL
32
+ ORDER BY d.updated_at DESC
33
+ `, [accountId]);
34
+ return result.rows.map((row) => ({
35
+ draftId: row.id,
36
+ title: row.title,
37
+ description: row.description,
38
+ repoOrg: row.repo_org,
39
+ repoName: row.repo_name,
40
+ repoHost: row.repo_host,
41
+ latestVersionNumber: row.latest_version_number === null ? null : Number(row.latest_version_number),
42
+ versionCount: Number(row.version_count),
43
+ createdAt: row.created_at,
44
+ updatedAt: row.updated_at,
45
+ latestVersionAt: row.latest_version_at,
46
+ disabled: Boolean(row.disabled_at),
47
+ publicUrl: getDraftPublicUrl({
48
+ draftId: row.id,
49
+ publicBaseUrl: config.publicBaseUrl,
50
+ requestBaseUrl
51
+ }),
52
+ rawUrl: getDraftRawUrl({
53
+ draftId: row.id,
54
+ publicBaseUrl: config.publicBaseUrl,
55
+ requestBaseUrl
56
+ })
57
+ }));
58
+ }
59
+ export async function getAccountDraftWithVersions(accountId, draftId, { requestBaseUrl }) {
60
+ const draftResult = await pool.query(`
61
+ SELECT *
62
+ FROM drafts
63
+ WHERE id = $1 AND account_id = $2 AND deleted_at IS NULL
64
+ LIMIT 1
65
+ `, [draftId, accountId]);
66
+ const draft = draftResult.rows[0];
67
+ if (!draft)
68
+ return null;
69
+ const versionsResult = await pool.query(`
70
+ SELECT id, version_number, created_at, git_branch, git_commit_sha,
71
+ git_commit_subject, git_dirty, file_size
72
+ FROM draft_versions
73
+ WHERE draft_id = $1 AND status = 'ready'
74
+ ORDER BY version_number DESC
75
+ `, [draftId]);
76
+ return {
77
+ draft: {
78
+ draftId: draft.id,
79
+ title: draft.title,
80
+ description: draft.description,
81
+ publicUrl: getDraftPublicUrl({
82
+ draftId: draft.id,
83
+ publicBaseUrl: config.publicBaseUrl,
84
+ requestBaseUrl
85
+ })
86
+ },
87
+ versions: versionsResult.rows
88
+ };
89
+ }
90
+ //# sourceMappingURL=drafts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../src/drafts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA+DpE,8EAA8E;AAC9E,6EAA6E;AAC7E,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAAiB,EACjB,EAAE,cAAc,EAA8B;IAE9C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B;;;;;;;;;;;;;;;;;;;;;;;;;KAyBC,EACD,CAAC,SAAS,CAAC,CACZ,CAAC;IAEF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,EAAE;QACf,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,OAAO,EAAE,GAAG,CAAC,QAAQ;QACrB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,mBAAmB,EACjB,GAAG,CAAC,qBAAqB,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC;QAC/E,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;QACvC,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,eAAe,EAAE,GAAG,CAAC,iBAAiB;QACtC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;QAClC,SAAS,EAAE,iBAAiB,CAAC;YAC3B,OAAO,EAAE,GAAG,CAAC,EAAE;YACf,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,cAAc;SACf,CAAC;QACF,MAAM,EAAE,cAAc,CAAC;YACrB,OAAO,EAAE,GAAG,CAAC,EAAE;YACf,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,cAAc;SACf,CAAC;KACH,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,SAAiB,EACjB,OAAe,EACf,EAAE,cAAc,EAA8B;IAE9C,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,KAAK,CAClC;;;;;KAKC,EACD,CAAC,OAAO,EAAE,SAAS,CAAC,CACrB,CAAC;IACF,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,KAAK,CACrC;;;;;;KAMC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO;QACL,KAAK,EAAE;YACL,OAAO,EAAE,KAAK,CAAC,EAAE;YACjB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,SAAS,EAAE,iBAAiB,CAAC;gBAC3B,OAAO,EAAE,KAAK,CAAC,EAAE;gBACjB,aAAa,EAAE,MAAM,CAAC,aAAa;gBACnC,cAAc;aACf,CAAC;SACH;QACD,QAAQ,EAAE,cAAc,CAAC,IAAI;KAC9B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface FeatureSetting {
2
+ enabled: boolean;
3
+ updatedByAccountId: string | null;
4
+ updatedAt: Date | string | null;
5
+ }
6
+ export declare function getBrowserLocalAnnotationsSetting(): Promise<FeatureSetting>;
7
+ export declare function setBrowserLocalAnnotationsEnabled(actorAccountId: string, enabled: boolean): Promise<FeatureSetting>;
@@ -0,0 +1,32 @@
1
+ import { newEventId, pool, withTransaction } from "./db.js";
2
+ const ANNOTATIONS_KEY = "browser_local_annotations";
3
+ export async function getBrowserLocalAnnotationsSetting() {
4
+ const result = await pool.query("SELECT enabled, updated_by_account_id, updated_at FROM site_settings WHERE key = $1 LIMIT 1", [ANNOTATIONS_KEY]);
5
+ const row = result.rows[0];
6
+ return row
7
+ ? { enabled: row.enabled, updatedByAccountId: row.updated_by_account_id, updatedAt: row.updated_at }
8
+ : { enabled: false, updatedByAccountId: null, updatedAt: null };
9
+ }
10
+ export async function setBrowserLocalAnnotationsEnabled(actorAccountId, enabled) {
11
+ return withTransaction(async (client) => {
12
+ const previous = await client.query("SELECT enabled, updated_by_account_id, updated_at FROM site_settings WHERE key = $1 FOR UPDATE", [ANNOTATIONS_KEY]);
13
+ const previousEnabled = previous.rows[0]?.enabled ?? null;
14
+ const result = await client.query(`
15
+ INSERT INTO site_settings (key, enabled, updated_by_account_id)
16
+ VALUES ($1, $2, $3)
17
+ ON CONFLICT (key) DO UPDATE
18
+ SET enabled = EXCLUDED.enabled,
19
+ updated_by_account_id = EXCLUDED.updated_by_account_id,
20
+ updated_at = now()
21
+ RETURNING enabled, updated_by_account_id, updated_at
22
+ `, [ANNOTATIONS_KEY, enabled, actorAccountId]);
23
+ await client.query(`
24
+ INSERT INTO admin_events (
25
+ id, actor_account_id, event_type, setting_key, previous_enabled, new_enabled
26
+ ) VALUES ($1, $2, $3, $4, $5, $6)
27
+ `, [newEventId(), actorAccountId, "feature_setting_changed", ANNOTATIONS_KEY, previousEnabled, enabled]);
28
+ const row = result.rows[0];
29
+ return { enabled: row.enabled, updatedByAccountId: row.updated_by_account_id, updatedAt: row.updated_at };
30
+ });
31
+ }
32
+ //# sourceMappingURL=feature-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-settings.js","sourceRoot":"","sources":["../src/feature-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG5D,MAAM,eAAe,GAAG,2BAA2B,CAAC;AAcpD,MAAM,CAAC,KAAK,UAAU,iCAAiC;IACrD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B,6FAA6F,EAC7F,CAAC,eAAe,CAAC,CAClB,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,OAAO,GAAG;QACR,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,CAAC,qBAAqB,EAAE,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE;QACpG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,cAAsB,EACtB,OAAgB;IAEhB,OAAO,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,gGAAgG,EAChG,CAAC,eAAe,CAAC,CAClB,CAAC;QACF,MAAM,eAAe,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC;QAC1D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B;;;;;;;;OAQC,EACD,CAAC,eAAe,EAAE,OAAO,EAAE,cAAc,CAAC,CAC3C,CAAC;QACF,MAAM,MAAM,CAAC,KAAK,CAChB;;;;OAIC,EACD,CAAC,UAAU,EAAE,EAAE,cAAc,EAAE,yBAAyB,EAAE,eAAe,EAAE,eAAe,EAAE,OAAO,CAAC,CACrG,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC;QAC5B,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,CAAC,qBAAqB,EAAE,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC;IAC5G,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const ANNOTATION_CLIENT: string;