@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
@@ -0,0 +1,439 @@
1
+ import { COPY_KEY_SCRIPT } from "./browser-security.js";
2
+ export function renderSignIn({ next, provider = "oidc", error, message }) {
3
+ const target = `/auth/sign-in?next=${encodeURIComponent(next || "/dashboard")}`;
4
+ const local = provider === "local";
5
+ return webPage({
6
+ title: "Sign in — Openplan",
7
+ body: `
8
+ <main class="narrow center">
9
+ <h1>Openplan</h1>
10
+ <p class="muted">Sign in to see the drafts you've published.</p>
11
+ ${message ? `<p class="success">${escapeHtml(message)}</p>` : ""}
12
+ ${error ? `<p class="error">${escapeHtml(error)}</p>` : ""}
13
+ ${local ? `
14
+ <form method="post" action="/auth/sign-in" autocomplete="on">
15
+ <input type="hidden" name="next" value="${escapeHtml(next || "/dashboard")}">
16
+ <p><input class="text-input" type="text" name="username" minlength="3" maxlength="64" required autofocus autocomplete="username" placeholder="Username"></p>
17
+ <p><input class="text-input" type="password" name="password" minlength="12" maxlength="512" required autocomplete="current-password" placeholder="Password"></p>
18
+ <p><button class="button" type="submit">Sign in</button></p>
19
+ </form>
20
+ ` : `<p><a class="button" href="${escapeHtml(target)}">Continue with SSO</a></p>`}
21
+ <p class="muted small">Publish with an Openplan API key to create private, account-owned plans. Anonymous publishing is available only when explicitly enabled by the operator.</p>
22
+ </main>
23
+ `
24
+ });
25
+ }
26
+ export function renderAccountSecurity({ session, local, message, error, isAdmin = false }) {
27
+ return webPage({
28
+ title: "Account security — Openplan",
29
+ header: pageHeader({ session, active: "account", isAdmin }),
30
+ body: `
31
+ <main class="narrow">
32
+ <h1>Account security</h1>
33
+ ${message ? `<p class="success">${escapeHtml(message)}</p>` : ""}
34
+ ${error ? `<p class="error">${escapeHtml(error)}</p>` : ""}
35
+ ${local ? `
36
+ <p class="muted">Changing your password revokes every existing browser session, including this one.</p>
37
+ <form method="post" action="/account/password">
38
+ <p><input class="text-input" type="password" name="currentPassword" minlength="12" maxlength="512" required autocomplete="current-password" placeholder="Current password"></p>
39
+ <p><input class="text-input" type="password" name="newPassword" minlength="12" maxlength="512" required autocomplete="new-password" placeholder="New password"></p>
40
+ <p><button class="button" type="submit">Change password and sign out everywhere</button></p>
41
+ </form>
42
+ ` : `<p class="muted">Your identity provider manages your sign-in and password.</p>`}
43
+ </main>
44
+ `
45
+ });
46
+ }
47
+ export function renderAdminUsers({ session, users, error }) {
48
+ const rows = users.map((user) => `
49
+ <tr>
50
+ <td>${escapeHtml(user.username)}</td>
51
+ <td>${escapeHtml(user.accountName)}</td>
52
+ <td>${user.isAdmin ? '<span class="pill ok">Administrator</span>' : "Member"}</td>
53
+ <td class="muted">${escapeHtml(formatDate(user.createdAt))}</td>
54
+ </tr>
55
+ `).join("\n");
56
+ return webPage({
57
+ title: "Local users — Openplan",
58
+ header: pageHeader({ session, active: "admin", isAdmin: true }),
59
+ body: `
60
+ <main>
61
+ <h1>Local users</h1>
62
+ <p><a href="/admin/features">Feature settings</a></p>
63
+ ${error ? `<p class="error">${escapeHtml(error)}</p>` : ""}
64
+ <section class="access-panel">
65
+ <h2>Create user</h2>
66
+ <form method="post" action="/admin/users">
67
+ <p><input class="text-input" type="text" name="username" minlength="3" maxlength="64" required autocomplete="off" placeholder="Username"></p>
68
+ <p><input class="text-input" type="text" name="displayName" maxlength="120" autocomplete="off" placeholder="Display name"></p>
69
+ <p><input class="text-input" type="password" name="password" minlength="12" maxlength="512" required autocomplete="new-password" placeholder="Initial password"></p>
70
+ <p><label><input type="checkbox" name="isAdmin" value="true"> Administrator</label></p>
71
+ <p><button class="button" type="submit">Create local user</button></p>
72
+ </form>
73
+ </section>
74
+ <table><tr><th>Username</th><th>Name</th><th>Role</th><th>Created</th></tr>${rows}</table>
75
+ </main>
76
+ `
77
+ });
78
+ }
79
+ export function renderAuthError({ message }) {
80
+ return webPage({
81
+ title: "Sign-in problem — Openplan",
82
+ body: `
83
+ <main class="narrow center">
84
+ <h1>Sign-in problem</h1>
85
+ <p class="muted">${escapeHtml(message)}</p>
86
+ <p><a class="button" href="/auth/sign-in">Try again</a></p>
87
+ </main>
88
+ `
89
+ });
90
+ }
91
+ export function renderSharePassword({ title, action, error }) {
92
+ return webPage({
93
+ title: `Password required — ${title}`,
94
+ body: `
95
+ <main class="narrow center">
96
+ <h1>Password required</h1>
97
+ <p class="muted">Enter the share password to view <strong>${escapeHtml(title)}</strong>. No Openplan account is required.</p>
98
+ ${error ? `<p class="error">${escapeHtml(error)}</p>` : ""}
99
+ <form method="post" action="${escapeHtml(action)}" autocomplete="off">
100
+ <p><input class="text-input" type="password" name="password" minlength="8" maxlength="512" required autofocus autocomplete="current-password"></p>
101
+ <p><button class="button" type="submit">View plan</button></p>
102
+ </form>
103
+ </main>
104
+ `
105
+ });
106
+ }
107
+ export function renderDashboard({ session, drafts, isAdmin = false }) {
108
+ const groups = groupByRepo(drafts);
109
+ const sections = groups
110
+ .map((group) => `
111
+ <section>
112
+ <h2>${escapeHtml(group.label)}${group.href
113
+ ? ` <a class="small repo-link" href="${escapeHtml(group.href)}" target="_blank" rel="noopener noreferrer">GitHub ↗</a>`
114
+ : ""}</h2>
115
+ ${group.drafts.map(renderDraftRow).join("\n")}
116
+ </section>
117
+ `)
118
+ .join("\n");
119
+ return webPage({
120
+ title: "My drafts — Openplan",
121
+ header: pageHeader({ session, active: "dashboard", isAdmin }),
122
+ body: `
123
+ <main>
124
+ <h1>My drafts</h1>
125
+ ${drafts.length
126
+ ? sections
127
+ : `<p class="muted">No drafts yet. Publish one with <code>openplan upload plan.html</code> using a key from <a href="/cli/auth">CLI setup</a>.</p>`}
128
+ </main>
129
+ `
130
+ });
131
+ }
132
+ export function renderAdminFeatures({ session, enabled, updatedAt, localUsers = false }) {
133
+ return webPage({
134
+ title: "Feature settings — Openplan",
135
+ header: pageHeader({ session, active: "admin", isAdmin: true }),
136
+ body: `
137
+ <main>
138
+ <h1>Feature settings</h1>
139
+ ${localUsers ? '<p><a href="/admin/users">Manage local users</a></p>' : ""}
140
+ <section class="access-panel">
141
+ <h2>Browser-local plan annotations</h2>
142
+ <p><strong>Status:</strong> ${enabled ? '<span class="pill ok">Enabled for all plans</span>' : '<span class="pill warn">Disabled</span>'}</p>
143
+ <p class="muted">When enabled, every otherwise-readable plan gains a separate annotation viewer. Drawings and highlights stay only in each reader's browser and are never synchronized or backed up by Openplan.</p>
144
+ ${updatedAt ? `<p class="muted small">Last changed ${escapeHtml(formatDate(updatedAt))} UTC.</p>` : ""}
145
+ <form method="post" action="/admin/features/browser-local-annotations">
146
+ <input type="hidden" name="enabled" value="${enabled ? "false" : "true"}">
147
+ <button class="button ${enabled ? "danger" : ""}" type="submit">${enabled ? "Disable for all plans" : "Enable for all plans"}</button>
148
+ </form>
149
+ <p class="muted small">Disabling the feature removes viewer entry points but does not erase annotation data already stored in readers' browsers.</p>
150
+ </section>
151
+ </main>
152
+ `
153
+ });
154
+ }
155
+ export function renderDraftDetail({ session, draft, versions, access, annotationsEnabled = false, isAdmin = false }) {
156
+ const rows = versions
157
+ .map((v) => {
158
+ const versionNumber = Number(v.version_number);
159
+ const versionAccess = access?.versions.get(versionNumber);
160
+ return `
161
+ <tr>
162
+ <td><a href="${escapeHtml(draft.publicUrl)}/v/${versionNumber}" target="_blank" rel="noopener noreferrer">v${versionNumber}</a>${annotationsEnabled ? ` · <a href="${escapeHtml(draft.publicUrl)}/v/${versionNumber}/annotate" target="_blank" rel="noopener noreferrer">annotate</a>` : ""}</td>
163
+ <td>${escapeHtml(v.git_commit_subject || "")}${v.git_dirty ? ' <span class="pill warn">dirty</span>' : ""}</td>
164
+ <td class="muted">${escapeHtml(v.git_branch || "")} ${escapeHtml((v.git_commit_sha || "").slice(0, 7))}</td>
165
+ <td class="muted">${escapeHtml(formatDate(v.created_at))}</td>
166
+ <td>${versionAccess ? renderVersionAccessForm(draft.draftId, versionNumber, versionAccess) : ""}</td>
167
+ </tr>
168
+ `;
169
+ })
170
+ .join("\n");
171
+ return webPage({
172
+ title: `${draft.title} — Openplan`,
173
+ header: pageHeader({ session, active: "dashboard", isAdmin }),
174
+ body: `
175
+ <main>
176
+ <p class="small"><a href="/dashboard">← My drafts</a></p>
177
+ <h1>${escapeHtml(draft.title)}</h1>
178
+ ${draft.description ? `<p class="muted">${escapeHtml(draft.description)}</p>` : ""}
179
+ <p><a href="${escapeHtml(draft.publicUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(draft.publicUrl)}</a>${annotationsEnabled ? ` · <a class="button compact" href="${escapeHtml(draft.publicUrl)}/annotate" target="_blank" rel="noopener noreferrer">Annotate current version</a>` : ""}</p>
180
+ ${access ? renderPlanAccessForm(draft.draftId, access.plan) : ""}
181
+ <table>
182
+ <tr><th>Version</th><th>Commit</th><th>Ref</th><th>Published</th><th>Access</th></tr>
183
+ ${rows}
184
+ </table>
185
+ </main>
186
+ `
187
+ });
188
+ }
189
+ function renderPlanAccessForm(draftId, access) {
190
+ return `
191
+ <section class="access-panel">
192
+ <h2>Plan sharing</h2>
193
+ <p class="muted small">New owned plans start private. Choose Public and save to allow anyone with the URL to read. Private permits the owner and explicitly linked accounts; password sharing requires an unlock password.</p>
194
+ <p class="muted small">Current default: <strong>${escapeHtml(accessLabel(access))}</strong>. Inherited versions change when this default changes.</p>
195
+ <form method="post" action="/dashboard/drafts/${escapeHtml(draftId)}/access">
196
+ <select name="mode">${renderAccessOptions(access, false)}</select>
197
+ <input class="password-input" type="password" name="password" minlength="8" maxlength="512" placeholder="Password when selecting protected">
198
+ <button class="button compact" type="submit">Update plan access</button>
199
+ </form>
200
+ <p class="muted small">Making a plan public cannot recall copies already downloaded or indexed. Enter a new password whenever selecting password protected.</p>
201
+ </section>
202
+ `;
203
+ }
204
+ function renderVersionAccessForm(draftId, versionNumber, access) {
205
+ return `
206
+ <form class="access-form" method="post" action="/dashboard/drafts/${escapeHtml(draftId)}/versions/${versionNumber}/access">
207
+ <span class="muted small">${escapeHtml(accessLabel(access))}</span>
208
+ <select name="mode">${renderAccessOptions(access, true)}</select>
209
+ <input class="password-input small-input" type="password" name="password" minlength="8" maxlength="512" placeholder="New password">
210
+ <button class="linklike" type="submit">Save</button>
211
+ </form>
212
+ `;
213
+ }
214
+ function renderAccessOptions(access, allowInherit) {
215
+ const selected = access.inherited ? "inherit" : access.mode;
216
+ const values = allowInherit
217
+ ? [["inherit", "Inherit plan"], ["private", "Private"], ["password", "Password protected"], ["public", "Public"]]
218
+ : [["private", "Private"], ["password", "Password protected"], ["public", "Public"]];
219
+ const unchanged = selected === "authenticated"
220
+ ? '<option value="" selected disabled>Keep current: Any signed-in user</option>' : "";
221
+ return unchanged + values.map(([value, label]) => `<option value="${value}"${selected === value ? " selected" : ""}>${label}</option>`).join("");
222
+ }
223
+ function accessLabel(access) {
224
+ const prefix = access.inherited ? "Inherited · " : access.legacy ? "Deployment default · " : "";
225
+ const mode = access.mode === "password" ? "Password protected" :
226
+ access.mode === "authenticated" ? "Any signed-in user" :
227
+ access.mode[0].toUpperCase() + access.mode.slice(1);
228
+ return `${prefix}${mode}`;
229
+ }
230
+ export function renderCliAuth({ session, keys = [], isAdmin = false }) {
231
+ const keyRows = keys
232
+ .map((key) => `
233
+ <tr>
234
+ <td>${escapeHtml(key.name)}</td>
235
+ <td class="muted">${escapeHtml(formatDate(key.created_at))}</td>
236
+ <td class="muted">${key.last_used_at ? escapeHtml(formatDate(key.last_used_at)) : "never used"}</td>
237
+ <td>
238
+ <form method="post" action="/cli/auth/keys/${escapeHtml(key.id)}/revoke">
239
+ <button class="linklike" type="submit">Revoke</button>
240
+ </form>
241
+ </td>
242
+ </tr>
243
+ `)
244
+ .join("\n");
245
+ return webPage({
246
+ title: "CLI setup — Openplan",
247
+ header: pageHeader({ session, active: "cli", isAdmin }),
248
+ body: `
249
+ <main class="narrow">
250
+ <h1>Connect your CLI</h1>
251
+ <p class="muted">Generate a key, then paste it into the waiting <code>openplan auth login</code> prompt in your terminal.</p>
252
+ <form method="post" action="/cli/auth/keys">
253
+ <button class="button" type="submit">Generate a new API key</button>
254
+ </form>
255
+ <p class="muted small">Each visit can mint a fresh key. Keys are shown once.</p>
256
+ ${keys.length
257
+ ? `<h2>Active keys</h2>
258
+ <table>
259
+ <tr><th>Name</th><th>Created</th><th>Last used</th><th></th></tr>
260
+ ${keyRows}
261
+ </table>`
262
+ : ""}
263
+ </main>
264
+ `
265
+ });
266
+ }
267
+ export function renderCliAuthKey({ session, token, keyName, isAdmin = false }) {
268
+ return webPage({
269
+ title: "Your new API key — Openplan",
270
+ header: pageHeader({ session, active: "cli", isAdmin }),
271
+ body: `
272
+ <main class="narrow">
273
+ <h1>Your new API key</h1>
274
+ <p class="muted">Named <strong>${escapeHtml(keyName)}</strong>. Shown once — copy it now and paste it into your terminal.</p>
275
+ <div class="keybox">
276
+ <code id="key">${escapeHtml(token)}</code>
277
+ <button class="button" id="copy" type="button">Copy</button>
278
+ </div>
279
+ <p class="muted small">Terminal: <code>openplan auth login</code> uses masked input. For automation, use <code>openplan auth set --stdin</code> with a protected input source. Verify the displayed endpoint; never put the key in command arguments.</p>
280
+ <script>${COPY_KEY_SCRIPT}</script>
281
+ </main>
282
+ `
283
+ });
284
+ }
285
+ function renderDraftRow(draft) {
286
+ // The title is the one-click "open the plan" action (new tab); the internal
287
+ // detail/version-history screen hangs off the separate Details link.
288
+ return `
289
+ <div class="row">
290
+ <div>
291
+ <a class="row-title" href="${escapeHtml(draft.publicUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(draft.title)}</a>
292
+ ${draft.disabled ? '<span class="pill warn">disabled</span>' : ""}
293
+ ${draft.description ? `<div class="muted small">${escapeHtml(draft.description)}</div>` : ""}
294
+ </div>
295
+ <div class="row-meta muted small">
296
+ <a href="/dashboard/drafts/${escapeHtml(draft.draftId)}">Details</a> ·
297
+ v${draft.latestVersionNumber ?? "—"} · ${draft.versionCount} version${draft.versionCount === 1 ? "" : "s"} · ${escapeHtml(formatDate(draft.updatedAt))}
298
+ </div>
299
+ </div>
300
+ `;
301
+ }
302
+ function groupByRepo(drafts) {
303
+ const map = new Map();
304
+ for (const draft of drafts) {
305
+ const hasRepo = draft.repoOrg && draft.repoName;
306
+ const key = hasRepo ? `${draft.repoOrg}/${draft.repoName}` : "";
307
+ if (!map.has(key)) {
308
+ // Drafts uploaded by pre-0.0.3 CLIs have no repo_host; default those to
309
+ // github.com so the one-click repo link still works. Any member draft
310
+ // with a recorded host upgrades the group's link below.
311
+ map.set(key, {
312
+ label: hasRepo ? key : "No repository",
313
+ href: hasRepo
314
+ ? `https://${draft.repoHost || "github.com"}/${draft.repoOrg}/${draft.repoName}`
315
+ : null,
316
+ drafts: []
317
+ });
318
+ }
319
+ const group = map.get(key);
320
+ if (hasRepo && draft.repoHost) {
321
+ group.href = `https://${draft.repoHost}/${draft.repoOrg}/${draft.repoName}`;
322
+ }
323
+ group.drafts.push(draft);
324
+ }
325
+ // Repo groups first (already newest-first within), "No repository" last.
326
+ return [...map.entries()]
327
+ .sort(([a], [b]) => (a === b ? 0 : a === "" ? 1 : b === "" ? -1 : 0))
328
+ .map(([, group]) => group);
329
+ }
330
+ function pageHeader({ session = {}, active, isAdmin = false }) {
331
+ // Claims arrive via shoo's verified id_token, but only render an avatar for
332
+ // plain https URLs anyway.
333
+ const avatar = typeof session.pictureUrl === "string" && session.pictureUrl.startsWith("https://")
334
+ ? `<img class="avatar" src="${escapeHtml(session.pictureUrl)}" alt="" referrerpolicy="no-referrer">`
335
+ : "";
336
+ // The email is PII: render it blurred until hover/focus (shoo's own /me
337
+ // pattern) so it never leaks on screenshares or streams. A user with no
338
+ // Google display name gets their email as the account name — in that case
339
+ // skip the plain name and show only the blurred email.
340
+ const name = session.accountName && session.accountName !== session.email
341
+ ? `<span class="muted small">${escapeHtml(session.accountName)}</span>`
342
+ : "";
343
+ const email = session.email
344
+ ? `<span class="muted small pii" title="Hover to reveal">${escapeHtml(session.email)}</span>`
345
+ : "";
346
+ const profileHref = isAdmin ? "/admin/features" : "/dashboard";
347
+ const profileTitle = isAdmin ? "Open feature settings" : "Open my drafts";
348
+ return `
349
+ <header class="top">
350
+ <nav>
351
+ <a href="/dashboard" class="${active === "dashboard" ? "active" : ""}">My drafts</a>
352
+ <a href="/cli/auth" class="${active === "cli" ? "active" : ""}">CLI setup</a>
353
+ <a href="/account/security" class="${active === "account" ? "active" : ""}">Security</a>
354
+ ${isAdmin ? `<a href="/admin/features" class="${active === "admin" ? "active" : ""}">Admin</a>` : ""}
355
+ </nav>
356
+ <div class="account-actions">
357
+ <a class="profile-link" href="${profileHref}" title="${profileTitle}">
358
+ ${avatar}
359
+ ${name}
360
+ ${email}
361
+ </a>
362
+ <form method="post" action="/auth/sign-out">
363
+ <button class="linklike" type="submit">Sign out</button>
364
+ </form>
365
+ </div>
366
+ </header>
367
+ `;
368
+ }
369
+ function formatDate(value) {
370
+ const date = new Date(value);
371
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 16).replace("T", " ");
372
+ }
373
+ function webPage({ title, body, header = "" }) {
374
+ return `<!doctype html>
375
+ <html lang="en">
376
+ <head>
377
+ <meta charset="utf-8">
378
+ <meta name="viewport" content="width=device-width, initial-scale=1">
379
+ <title>${escapeHtml(title)}</title>
380
+ <style>
381
+ body { margin: 0; background: #f8fafc; color: #111827; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
382
+ main { max-width: 860px; margin: 32px auto 80px; padding: 0 20px; }
383
+ main.narrow { max-width: 560px; }
384
+ main.center { text-align: center; margin-top: 96px; }
385
+ h1 { font-size: 30px; margin: 0 0 14px; }
386
+ h2 { font-size: 17px; margin: 30px 0 6px; }
387
+ p { line-height: 1.6; }
388
+ a { color: #1d4ed8; }
389
+ code { background: #eef2f7; border: 1px solid #d1d5db; border-radius: 5px; padding: 1px 5px; font-size: 14px; }
390
+ .muted { color: #6b7280; }
391
+ .small { font-size: 13px; }
392
+ .button { display: inline-block; background: #111827; color: #fff; border: 0; border-radius: 8px; padding: 10px 18px; font-size: 15px; text-decoration: none; cursor: pointer; }
393
+ .linklike { background: none; border: 0; color: #1d4ed8; cursor: pointer; font-size: 13px; padding: 0; margin-left: 10px; text-decoration: underline; }
394
+ .top { display: flex; justify-content: space-between; align-items: center; max-width: 860px; margin: 0 auto; padding: 14px 20px; border-bottom: 1px solid #e5e7eb; }
395
+ .top nav a { margin-right: 16px; text-decoration: none; color: #374151; }
396
+ .top nav a.active { color: #111827; font-weight: 600; }
397
+ .account-actions, .profile-link, .top form { display: inline-flex; align-items: center; gap: 8px; }
398
+ .profile-link { color: inherit; text-decoration: none; border-radius: 8px; padding: 4px 6px; }
399
+ .profile-link:hover, .profile-link:focus { background: #eef2f7; outline: none; }
400
+ .avatar { width: 26px; height: 26px; border-radius: 50%; vertical-align: middle; }
401
+ .repo-link { font-weight: 400; text-decoration: none; margin-left: 6px; }
402
+ .row-meta a { color: #6b7280; }
403
+ .pii { filter: blur(4px); border-radius: 3px; transition: filter .15s ease; cursor: pointer; }
404
+ .pii:hover, .profile-link:focus .pii { filter: none; outline: none; }
405
+ .row { display: flex; justify-content: space-between; gap: 14px; align-items: baseline; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }
406
+ .row-title { font-weight: 600; text-decoration: none; }
407
+ .row-meta { white-space: nowrap; }
408
+ .pill { font-size: 11px; border-radius: 999px; padding: 2px 8px; margin-left: 6px; }
409
+ .pill.warn { background: #fef3c7; color: #92400e; }
410
+ .pill.ok { background: #dcfce7; color: #166534; }
411
+ .button.danger { background: #991b1b; }
412
+ .text-input { width: min(100%, 420px); border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px; font: inherit; }
413
+ .error { color: #991b1b; }
414
+ .success { color: #166534; }
415
+ table { width: 100%; border-collapse: collapse; margin-top: 14px; }
416
+ th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
417
+ th { color: #6b7280; font-weight: 600; font-size: 12px; text-transform: uppercase; }
418
+ .keybox { display: flex; gap: 10px; align-items: center; background: #fff; border: 1px solid #d1d5db; border-radius: 8px; padding: 14px; margin: 14px 0; }
419
+ .keybox code { flex: 1; word-break: break-all; background: none; border: 0; font-size: 15px; }
420
+ .text-input { width: 100%; max-width: 360px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px; font-size: 16px; }
421
+ .error { color: #b91c1c; }
422
+ .access-panel { margin: 24px 0; padding: 18px; background: #fff; border: 1px solid #d1d5db; border-radius: 10px; }
423
+ .access-panel form, .access-form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
424
+ select, .password-input { border: 1px solid #cbd5e1; border-radius: 7px; padding: 7px 9px; background: #fff; }
425
+ .small-input { max-width: 145px; }
426
+ .compact { padding: 8px 12px; font-size: 13px; }
427
+ </style>
428
+ </head>
429
+ <body>${header}${body}</body>
430
+ </html>`;
431
+ }
432
+ function escapeHtml(value) {
433
+ return String(value)
434
+ .replaceAll("&", "&amp;")
435
+ .replaceAll("<", "&lt;")
436
+ .replaceAll(">", "&gt;")
437
+ .replaceAll('"', "&quot;");
438
+ }
439
+ //# sourceMappingURL=render-web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-web.js","sourceRoot":"","sources":["../src/render-web.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function renderHome({ publicBaseUrl }: {
2
+ publicBaseUrl: string | undefined;
3
+ }): string;
4
+ export declare function renderNotFound(): string;
package/dist/render.js ADDED
@@ -0,0 +1,85 @@
1
+ export function renderHome({ publicBaseUrl }) {
2
+ const apiUrl = publicBaseUrl || "<your-openplan-url>";
3
+ return htmlPage({
4
+ title: "Openplan",
5
+ body: `
6
+ <main class="home">
7
+ <h1>Openplan</h1>
8
+ <p>Authenticated static HTML draft publishing for agents.</p>
9
+ <p>New account-owned plans are private. To share publicly, open the plan in My drafts, select Public in Plan access, and save. Existing plans keep their sharing settings.</p>
10
+ <p>Configure this Openplan endpoint, authenticate, then upload:</p>
11
+ <pre>openplan endpoint set ${escapeHtml(apiUrl)}
12
+ openplan auth login
13
+ openplan whoami
14
+ openplan upload ./plan.html</pre>
15
+ <p><a href="/dashboard">My drafts</a> · <a href="/cli/auth">CLI setup</a></p>
16
+ <p>Health: <a href="/healthz">/healthz</a></p>
17
+ <p>Public base URL: ${escapeHtml(publicBaseUrl || "not configured")}</p>
18
+ </main>
19
+ `
20
+ });
21
+ }
22
+ export function renderNotFound() {
23
+ return htmlPage({
24
+ title: "Draft not found",
25
+ body: `
26
+ <main class="home">
27
+ <h1>Draft not found</h1>
28
+ <p>The requested draft is unavailable.</p>
29
+ </main>
30
+ `
31
+ });
32
+ }
33
+ function htmlPage({ title, body }) {
34
+ return `<!doctype html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <title>${escapeHtml(title)}</title>
40
+ <style>
41
+ body {
42
+ margin: 0;
43
+ background: #f8fafc;
44
+ color: #111827;
45
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
46
+ }
47
+
48
+ .home {
49
+ max-width: 760px;
50
+ margin: 64px auto;
51
+ padding: 0 20px;
52
+ }
53
+
54
+ h1 {
55
+ margin: 0 0 12px;
56
+ font-size: 40px;
57
+ line-height: 1.1;
58
+ }
59
+
60
+ p {
61
+ color: #374151;
62
+ font-size: 17px;
63
+ line-height: 1.6;
64
+ }
65
+
66
+ pre {
67
+ overflow-x: auto;
68
+ padding: 14px;
69
+ border: 1px solid #d1d5db;
70
+ background: #ffffff;
71
+ border-radius: 6px;
72
+ }
73
+ </style>
74
+ </head>
75
+ <body>${body}</body>
76
+ </html>`;
77
+ }
78
+ function escapeHtml(value) {
79
+ return String(value)
80
+ .replaceAll("&", "&amp;")
81
+ .replaceAll("<", "&lt;")
82
+ .replaceAll(">", "&gt;")
83
+ .replaceAll('"', "&quot;");
84
+ }
85
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,EAAE,aAAa,EAAyC;IACjF,MAAM,MAAM,GAAG,aAAa,IAAI,qBAAqB,CAAC;IACtD,OAAO,QAAQ,CAAC;QACd,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE;;;;;;qCAM2B,UAAU,CAAC,MAAM,CAAC;;;;;;8BAMzB,UAAU,CAAC,aAAa,IAAI,gBAAgB,CAAC;;KAEtE;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,QAAQ,CAAC;QACd,KAAK,EAAE,iBAAiB;QACxB,IAAI,EAAE;;;;;KAKL;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAmC;IAChE,OAAO;;;;;WAKE,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAoCpB,IAAI;QACJ,CAAC;AACT,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,MAAM,CAAC,KAAK,CAAC;SACjB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,26 @@
1
+ import { createApp } from "./api.js";
2
+ import { assertRuntimeConfigured, config } from "./config.js";
3
+ import { ensureBootstrapApiKey, initDb } from "./db.js";
4
+ import { runReconciliation } from "./reconcile.js";
5
+ import { assertStorageConfigured } from "./storage.js";
6
+ async function main() {
7
+ assertRuntimeConfigured();
8
+ assertStorageConfigured();
9
+ await initDb();
10
+ await ensureBootstrapApiKey();
11
+ const app = createApp();
12
+ app.listen(config.port, () => {
13
+ console.log(`Openplan listening on port ${config.port} (${config.runtimeProfile} profile)`);
14
+ });
15
+ if (config.reconcileIntervalMs > 0) {
16
+ const timer = setInterval(() => void runReconciliation().catch((error) => {
17
+ console.error("openplan reconciliation failed", error);
18
+ }), config.reconcileIntervalMs);
19
+ timer.unref();
20
+ }
21
+ }
22
+ main().catch((error) => {
23
+ console.error(error);
24
+ process.exit(1);
25
+ });
26
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD,KAAK,UAAU,IAAI;IACjB,uBAAuB,EAAE,CAAC;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,MAAM,MAAM,EAAE,CAAC;IACf,MAAM,qBAAqB,EAAE,CAAC;IAE9B,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QAC3B,OAAO,CAAC,GAAG,CAAC,8BAA8B,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,cAAc,WAAW,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,mBAAmB,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,iBAAiB,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACvE,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAC;QACzD,CAAC,CAAC,EAAE,MAAM,CAAC,mBAAmB,CAAC,CAAC;QAChC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/shoo.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { type JWTPayload } from "jose";
2
+ export interface PkceValues {
3
+ verifier: string;
4
+ challenge: string;
5
+ state: string;
6
+ }
7
+ export interface ShooTokenResponse extends Record<string, unknown> {
8
+ id_token: string;
9
+ }
10
+ export type ShooClaims = JWTPayload & {
11
+ pairwise_sub: string;
12
+ };
13
+ export declare function buildPkce(): PkceValues;
14
+ export declare function buildAuthorizeUrl({ redirectUri, state, challenge }: {
15
+ redirectUri: string;
16
+ state: string;
17
+ challenge: string;
18
+ }): string;
19
+ export declare function exchangeCode({ code, verifier, redirectUri }: {
20
+ code: string;
21
+ verifier: string;
22
+ redirectUri: string;
23
+ }): Promise<ShooTokenResponse>;
24
+ export declare function verifyIdToken(idToken: string, { audOrigin }: {
25
+ audOrigin: string;
26
+ }): Promise<ShooClaims>;