@rubytech/create-maxy-code 0.1.458 → 0.1.460
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/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -3
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +79 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
- package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +2 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
- package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
- package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
- package/payload/platform/lib/storage-broker/src/index.ts +2 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
- package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/references/api.md +1 -1
- package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
- package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
- package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
- package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
- package/payload/platform/scripts/logs-read.sh +63 -9
- package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
- package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +412 -37
- package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Handler, Logger, PortalEnv, R2ObjectBody } from './_lib/types'
|
|
2
|
+
import { readSessionCookie, resolveSession } from './_lib/session'
|
|
3
|
+
import { q } from './_lib/log'
|
|
4
|
+
import { authorizeKey } from './_lib/authorize'
|
|
5
|
+
|
|
6
|
+
interface DownloadResult extends Handler {
|
|
7
|
+
body?: R2ObjectBody | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function processDownload(
|
|
11
|
+
sessionId: string,
|
|
12
|
+
key: string,
|
|
13
|
+
env: PortalEnv,
|
|
14
|
+
log: Logger,
|
|
15
|
+
nowMs: number,
|
|
16
|
+
): Promise<DownloadResult> {
|
|
17
|
+
const session = await resolveSession(env.DB, sessionId, nowMs)
|
|
18
|
+
if (!session) {
|
|
19
|
+
log('[data-portal] op=download owner=none result=denied')
|
|
20
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
21
|
+
}
|
|
22
|
+
// Authorize BEFORE any R2 call, so a denied caller cannot cause a fetch.
|
|
23
|
+
if (!authorizeKey(session.ownerId, key)) {
|
|
24
|
+
log(`[data-portal] op=download owner=${q(session.ownerId)} result=denied reason=not-owner`)
|
|
25
|
+
return { status: 403, payload: { ok: false, error: 'denied' } }
|
|
26
|
+
}
|
|
27
|
+
const obj = await env.BUCKET.get(key)
|
|
28
|
+
if (!obj) {
|
|
29
|
+
log(`[data-portal] op=download owner=${q(session.ownerId)} result=absent`)
|
|
30
|
+
return { status: 404, payload: { ok: false, error: 'not found' } }
|
|
31
|
+
}
|
|
32
|
+
log(`[data-portal] op=download owner=${q(session.ownerId)} result=ok bytes=${obj.size ?? 'na'}`)
|
|
33
|
+
return { status: 200, payload: { ok: true }, body: obj }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PagesContext {
|
|
37
|
+
request: Request
|
|
38
|
+
env: PortalEnv
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function onRequestGet(context: PagesContext): Promise<Response> {
|
|
42
|
+
const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
|
|
43
|
+
const key = new URL(context.request.url).searchParams.get('key') ?? ''
|
|
44
|
+
const { status, payload, body } = await processDownload(
|
|
45
|
+
sessionId,
|
|
46
|
+
key,
|
|
47
|
+
context.env,
|
|
48
|
+
(line) => console.log(line),
|
|
49
|
+
Date.now(),
|
|
50
|
+
)
|
|
51
|
+
if (status === 200 && body) {
|
|
52
|
+
return new Response(body.body, {
|
|
53
|
+
status: 200,
|
|
54
|
+
headers: {
|
|
55
|
+
// RFC 6266 filename*: percent-encoded, so a name containing a quote
|
|
56
|
+
// cannot break out of the quoted form, and CR/LF cannot reach the
|
|
57
|
+
// Headers constructor (which would throw and 500 the download).
|
|
58
|
+
'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(key.split('/').pop() ?? 'file')}`,
|
|
59
|
+
// `attachment` already stops inline rendering; nosniff is the belt to
|
|
60
|
+
// that braces, so an uploaded .html can never be sniffed and executed
|
|
61
|
+
// on the portal's own origin.
|
|
62
|
+
'x-content-type-options': 'nosniff',
|
|
63
|
+
// These are private client documents. Without this a browser or an
|
|
64
|
+
// intermediary proxy may cache one person's invoice.
|
|
65
|
+
'cache-control': 'private, no-store',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
return Response.json(payload, { status })
|
|
70
|
+
}
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Handler, Logger, PortalEnv } from './_lib/types'
|
|
2
|
+
import { readSessionCookie, resolveSession } from './_lib/session'
|
|
3
|
+
import { q } from './_lib/log'
|
|
4
|
+
|
|
5
|
+
export async function processFiles(
|
|
6
|
+
sessionId: string,
|
|
7
|
+
env: PortalEnv,
|
|
8
|
+
log: Logger,
|
|
9
|
+
nowMs: number,
|
|
10
|
+
): Promise<Handler> {
|
|
11
|
+
const session = await resolveSession(env.DB, sessionId, nowMs)
|
|
12
|
+
if (!session) {
|
|
13
|
+
log('[data-portal] op=list owner=none result=denied')
|
|
14
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
15
|
+
}
|
|
16
|
+
// Scoped by ownerId, not by key prefix: the owner is the authority.
|
|
17
|
+
const rows = await env.DB.prepare(
|
|
18
|
+
'SELECT fileId, filename, objectKey, size, uploadedAt, ingested FROM manifest WHERE ownerId = ? ORDER BY uploadedAt DESC',
|
|
19
|
+
)
|
|
20
|
+
.bind(session.ownerId)
|
|
21
|
+
.all<Record<string, unknown>>()
|
|
22
|
+
const files = rows.results ?? []
|
|
23
|
+
log(`[data-portal] op=list owner=${q(session.ownerId)} result=ok files=${files.length}`)
|
|
24
|
+
return { status: 200, payload: { ok: true, files } }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PagesContext {
|
|
28
|
+
request: Request
|
|
29
|
+
env: PortalEnv
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function onRequestGet(context: PagesContext): Promise<Response> {
|
|
33
|
+
const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
|
|
34
|
+
const { status, payload } = await processFiles(
|
|
35
|
+
sessionId,
|
|
36
|
+
context.env,
|
|
37
|
+
(line) => console.log(line),
|
|
38
|
+
Date.now(),
|
|
39
|
+
)
|
|
40
|
+
return Response.json(payload, { status })
|
|
41
|
+
}
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Handler, Logger, PortalEnv } from './_lib/types'
|
|
2
|
+
import { readSessionCookie, resolveSession } from './_lib/session'
|
|
3
|
+
import { q } from './_lib/log'
|
|
4
|
+
import { authorizeKey, ownerPrefix } from './_lib/authorize'
|
|
5
|
+
|
|
6
|
+
const MAX_FILENAME = 200
|
|
7
|
+
|
|
8
|
+
// An upload is read wholly into the Function's memory, so it needs a bound or a
|
|
9
|
+
// large one OOMs the isolate — and an OOM emits no [data-portal] line at all,
|
|
10
|
+
// making it exactly the unobservable failure this portal's lifeline exists to
|
|
11
|
+
// prevent. 25 MiB is chosen for the documents this portal is for (invoices,
|
|
12
|
+
// photos, spreadsheets) and is deliberately NOT the storage broker's 100 MiB
|
|
13
|
+
// (task 1695): that one bounds a house-process spike, this one bounds an edge
|
|
14
|
+
// isolate, and the two must move independently.
|
|
15
|
+
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
export async function processUpload(
|
|
18
|
+
sessionId: string,
|
|
19
|
+
filename: string,
|
|
20
|
+
bytes: Uint8Array,
|
|
21
|
+
env: PortalEnv,
|
|
22
|
+
log: Logger,
|
|
23
|
+
newId: () => string,
|
|
24
|
+
now: () => number,
|
|
25
|
+
): Promise<Handler> {
|
|
26
|
+
// `now` is a clock, not a scalar: op=complete reports elapsed time, and a
|
|
27
|
+
// single injected instant cannot measure elapsed anything. It previously
|
|
28
|
+
// printed a hardcoded ms=0, which an operator reads as "took no time" rather
|
|
29
|
+
// than "was never measured".
|
|
30
|
+
const startedMs = now()
|
|
31
|
+
const session = await resolveSession(env.DB, sessionId, startedMs)
|
|
32
|
+
if (!session) {
|
|
33
|
+
log('[data-portal] op=upload owner=none result=denied')
|
|
34
|
+
return { status: 401, payload: { ok: false, error: 'denied' } }
|
|
35
|
+
}
|
|
36
|
+
const owner = session.ownerId
|
|
37
|
+
const uploadId = newId()
|
|
38
|
+
|
|
39
|
+
if (!filename || filename.length > MAX_FILENAME) {
|
|
40
|
+
log(`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
41
|
+
return { status: 400, payload: { ok: false, error: 'filename required' } }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = `${ownerPrefix(owner)}${filename}`
|
|
45
|
+
// The key is derived from the session owner, then re-checked through the same
|
|
46
|
+
// gate the other routes use — a filename carrying '..' must not escape.
|
|
47
|
+
if (!authorizeKey(owner, key)) {
|
|
48
|
+
log(`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
|
|
49
|
+
return { status: 400, payload: { ok: false, error: 'invalid filename' } }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
53
|
+
log(
|
|
54
|
+
`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)} bytes=${bytes.byteLength} result=too-large limit=${MAX_UPLOAD_BYTES}`,
|
|
55
|
+
)
|
|
56
|
+
return {
|
|
57
|
+
status: 413,
|
|
58
|
+
payload: {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `file is ${bytes.byteLength} bytes, over the ${MAX_UPLOAD_BYTES}-byte limit`,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log(
|
|
66
|
+
`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)} bytes=${bytes.byteLength}`,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await env.BUCKET.put(key, bytes)
|
|
71
|
+
log(`[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} result=ok`)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log(
|
|
74
|
+
`[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} result=failed err="${(err as Error).message}"`,
|
|
75
|
+
)
|
|
76
|
+
return { status: 502, payload: { ok: false, error: 'store failed' } }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fileId = newId()
|
|
80
|
+
try {
|
|
81
|
+
// Upsert, not insert. R2 overwrote the object above unconditionally, so a
|
|
82
|
+
// re-upload of the same filename has already replaced the file whatever the
|
|
83
|
+
// manifest does; a plain INSERT would hit `objectKey UNIQUE`, throw, and
|
|
84
|
+
// leave the row describing bytes that no longer exist while telling the
|
|
85
|
+
// person their upload failed. Resetting `ingested` is the load-bearing
|
|
86
|
+
// part: without it a replacement of an already-swept file would never be
|
|
87
|
+
// ingested, and nothing would ever say so.
|
|
88
|
+
await env.DB.prepare(
|
|
89
|
+
`INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested)
|
|
90
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
91
|
+
ON CONFLICT (objectKey) DO UPDATE SET
|
|
92
|
+
fileId = excluded.fileId,
|
|
93
|
+
size = excluded.size,
|
|
94
|
+
uploadedAt = excluded.uploadedAt,
|
|
95
|
+
ingested = 0`,
|
|
96
|
+
)
|
|
97
|
+
.bind(fileId, owner, filename, key, bytes.byteLength, new Date(startedMs).toISOString(), 0)
|
|
98
|
+
.run()
|
|
99
|
+
log(`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=ok`)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log(
|
|
102
|
+
`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=failed err="${(err as Error).message}"`,
|
|
103
|
+
)
|
|
104
|
+
return { status: 502, payload: { ok: false, error: 'manifest failed' } }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The verified post-condition: re-read both sides rather than assume the
|
|
108
|
+
// writes above landed. An orphan object or a phantom row emits no error at
|
|
109
|
+
// request time, which is why this line exists.
|
|
110
|
+
const objectPresent = (await env.BUCKET.head(key)) !== null
|
|
111
|
+
const rowPresent =
|
|
112
|
+
(await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?').bind(key).first()) !==
|
|
113
|
+
null
|
|
114
|
+
log(
|
|
115
|
+
`[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} objectPresent=${objectPresent} rowPresent=${rowPresent}`,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return { status: 200, payload: { ok: true, fileId, objectPresent, rowPresent } }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface PagesContext {
|
|
122
|
+
request: Request
|
|
123
|
+
env: PortalEnv
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function onRequestPost(context: PagesContext): Promise<Response> {
|
|
127
|
+
const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
|
|
128
|
+
let filename = ''
|
|
129
|
+
let bytes: Uint8Array
|
|
130
|
+
try {
|
|
131
|
+
const form = await context.request.formData()
|
|
132
|
+
const file = form.get('file')
|
|
133
|
+
if (!(file instanceof File)) {
|
|
134
|
+
return Response.json({ ok: false, error: 'file required' }, { status: 400 })
|
|
135
|
+
}
|
|
136
|
+
filename = file.name
|
|
137
|
+
bytes = new Uint8Array(await file.arrayBuffer())
|
|
138
|
+
} catch {
|
|
139
|
+
return Response.json({ ok: false, error: 'invalid form' }, { status: 400 })
|
|
140
|
+
}
|
|
141
|
+
const { status, payload } = await processUpload(
|
|
142
|
+
sessionId,
|
|
143
|
+
filename,
|
|
144
|
+
bytes,
|
|
145
|
+
context.env,
|
|
146
|
+
(line) => console.log(line),
|
|
147
|
+
() => crypto.randomUUID(),
|
|
148
|
+
() => Date.now(),
|
|
149
|
+
)
|
|
150
|
+
return Response.json(payload, { status })
|
|
151
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>File drop</title>
|
|
7
|
+
<!-- Same-origin: the API is served by this project's own Pages Functions,
|
|
8
|
+
so api-base is deliberately empty (Task 1307's convention). -->
|
|
9
|
+
<meta name="api-base" content="" />
|
|
10
|
+
<link rel="stylesheet" href="/portal.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main class="dp-card">
|
|
14
|
+
<h1 class="dp-title">File drop</h1>
|
|
15
|
+
<p id="dp-status" class="dp-status" role="status"></p>
|
|
16
|
+
|
|
17
|
+
<form id="dp-login" class="dp-login">
|
|
18
|
+
<label class="dp-label" for="dp-owner">Your name</label>
|
|
19
|
+
<input class="dp-input" id="dp-owner" name="owner" autocomplete="username" required />
|
|
20
|
+
<label class="dp-label" for="dp-passcode">Passcode</label>
|
|
21
|
+
<input
|
|
22
|
+
class="dp-input"
|
|
23
|
+
id="dp-passcode"
|
|
24
|
+
name="passcode"
|
|
25
|
+
type="password"
|
|
26
|
+
autocomplete="current-password"
|
|
27
|
+
required
|
|
28
|
+
/>
|
|
29
|
+
<button class="dp-btn" type="submit">Sign in</button>
|
|
30
|
+
</form>
|
|
31
|
+
|
|
32
|
+
<section id="dp-panel" class="dp-panel" hidden>
|
|
33
|
+
<form id="dp-upload" class="dp-upload">
|
|
34
|
+
<input class="dp-file" id="dp-file" type="file" required />
|
|
35
|
+
<button class="dp-btn" type="submit">Upload</button>
|
|
36
|
+
</form>
|
|
37
|
+
<ul id="dp-files" class="dp-files"></ul>
|
|
38
|
+
</section>
|
|
39
|
+
</main>
|
|
40
|
+
<script src="/portal.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* Data portal styles. Plain CSS, dp- prefixed, themed off variables so a brand
|
|
2
|
+
can restyle without touching markup — the same shape as booking.css. */
|
|
3
|
+
:root {
|
|
4
|
+
--dp-ink: #14181f;
|
|
5
|
+
--dp-muted: #667085;
|
|
6
|
+
--dp-paper: #ffffff;
|
|
7
|
+
--dp-rule: #e4e7ec;
|
|
8
|
+
--dp-accent: #1f6feb;
|
|
9
|
+
--dp-accent-ink: #ffffff;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 2rem 1rem;
|
|
19
|
+
background: var(--dp-paper);
|
|
20
|
+
color: var(--dp-ink);
|
|
21
|
+
font: 16px/1.5 system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.dp-card {
|
|
25
|
+
max-width: 34rem;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dp-title {
|
|
30
|
+
margin: 0 0 1rem;
|
|
31
|
+
font-size: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.dp-status {
|
|
35
|
+
min-height: 1.5rem;
|
|
36
|
+
margin: 0 0 1rem;
|
|
37
|
+
color: var(--dp-muted);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dp-login,
|
|
41
|
+
.dp-upload {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dp-label {
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
color: var(--dp-muted);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dp-input,
|
|
53
|
+
.dp-file {
|
|
54
|
+
padding: 0.625rem 0.75rem;
|
|
55
|
+
border: 1px solid var(--dp-rule);
|
|
56
|
+
border-radius: 0.375rem;
|
|
57
|
+
font: inherit;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dp-btn {
|
|
61
|
+
margin-top: 0.5rem;
|
|
62
|
+
padding: 0.625rem 1rem;
|
|
63
|
+
border: 0;
|
|
64
|
+
border-radius: 0.375rem;
|
|
65
|
+
background: var(--dp-accent);
|
|
66
|
+
color: var(--dp-accent-ink);
|
|
67
|
+
font: inherit;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dp-upload {
|
|
72
|
+
margin-bottom: 1.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dp-files {
|
|
76
|
+
list-style: none;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.dp-file-row {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: space-between;
|
|
85
|
+
gap: 1rem;
|
|
86
|
+
padding: 0.75rem 0;
|
|
87
|
+
border-bottom: 1px solid var(--dp-rule);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dp-file-name {
|
|
91
|
+
color: var(--dp-accent);
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
word-break: break-all;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dp-file-name:hover {
|
|
97
|
+
text-decoration: underline;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.dp-del {
|
|
101
|
+
border: 1px solid var(--dp-rule);
|
|
102
|
+
border-radius: 0.375rem;
|
|
103
|
+
background: transparent;
|
|
104
|
+
color: var(--dp-muted);
|
|
105
|
+
padding: 0.375rem 0.625rem;
|
|
106
|
+
font: inherit;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dp-empty {
|
|
111
|
+
color: var(--dp-muted);
|
|
112
|
+
padding: 0.75rem 0;
|
|
113
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* Data portal client. Dependency-free, no bundler, no framework — the same
|
|
2
|
+
shape as the booking page's booking.js.
|
|
3
|
+
|
|
4
|
+
Three steps: sign in with a passcode, list your own files, upload/delete.
|
|
5
|
+
The session cookie is HttpOnly, so this script never sees or stores it. */
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
function meta(name) {
|
|
10
|
+
var el = document.querySelector('meta[name="' + name + '"]');
|
|
11
|
+
return el ? el.getAttribute('content') || '' : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var API = meta('api-base');
|
|
15
|
+
var statusEl = document.getElementById('dp-status');
|
|
16
|
+
var loginEl = document.getElementById('dp-login');
|
|
17
|
+
var panelEl = document.getElementById('dp-panel');
|
|
18
|
+
var filesEl = document.getElementById('dp-files');
|
|
19
|
+
|
|
20
|
+
function say(msg) {
|
|
21
|
+
statusEl.textContent = msg;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function api(path, opts) {
|
|
25
|
+
return fetch(API + path, Object.assign({ credentials: 'same-origin' }, opts || {}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function item(text) {
|
|
29
|
+
var li = document.createElement('li');
|
|
30
|
+
li.className = 'dp-empty';
|
|
31
|
+
li.textContent = text;
|
|
32
|
+
return li;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function render(files) {
|
|
36
|
+
// replaceChildren, not innerHTML: every filename below is set with
|
|
37
|
+
// textContent so a name like '<img onerror=…>' renders as text, and
|
|
38
|
+
// clearing the list should not go anywhere near HTML parsing either.
|
|
39
|
+
filesEl.replaceChildren();
|
|
40
|
+
if (!files.length) {
|
|
41
|
+
filesEl.appendChild(item('No files yet.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
files.forEach(function (f) {
|
|
45
|
+
var li = document.createElement('li');
|
|
46
|
+
li.className = 'dp-file-row';
|
|
47
|
+
|
|
48
|
+
var a = document.createElement('a');
|
|
49
|
+
a.className = 'dp-file-name';
|
|
50
|
+
a.href = API + '/api/download?key=' + encodeURIComponent(f.objectKey);
|
|
51
|
+
a.textContent = f.filename;
|
|
52
|
+
li.appendChild(a);
|
|
53
|
+
|
|
54
|
+
var del = document.createElement('button');
|
|
55
|
+
del.className = 'dp-del';
|
|
56
|
+
del.type = 'button';
|
|
57
|
+
del.textContent = 'Delete';
|
|
58
|
+
del.addEventListener('click', function () {
|
|
59
|
+
remove(f.objectKey);
|
|
60
|
+
});
|
|
61
|
+
li.appendChild(del);
|
|
62
|
+
|
|
63
|
+
filesEl.appendChild(li);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function refresh() {
|
|
68
|
+
return api('/api/files')
|
|
69
|
+
.then(function (r) {
|
|
70
|
+
return r.json();
|
|
71
|
+
})
|
|
72
|
+
.then(function (d) {
|
|
73
|
+
if (d.ok) render(d.files || []);
|
|
74
|
+
})
|
|
75
|
+
.catch(function () {
|
|
76
|
+
say('Could not load your files.');
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function remove(key) {
|
|
81
|
+
say('Deleting…');
|
|
82
|
+
api('/api/delete', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'content-type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ key: key }),
|
|
86
|
+
})
|
|
87
|
+
.then(function (r) {
|
|
88
|
+
return r.json();
|
|
89
|
+
})
|
|
90
|
+
.then(function (d) {
|
|
91
|
+
say(d.ok ? 'Deleted.' : 'Could not delete that.');
|
|
92
|
+
return refresh();
|
|
93
|
+
})
|
|
94
|
+
.catch(function () {
|
|
95
|
+
say('Could not delete that.');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
loginEl.addEventListener('submit', function (e) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
say('Signing in…');
|
|
102
|
+
api('/api/auth', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'content-type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
ownerId: document.getElementById('dp-owner').value.trim(),
|
|
107
|
+
passcode: document.getElementById('dp-passcode').value,
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
.then(function (r) {
|
|
111
|
+
return r.json().then(function (d) {
|
|
112
|
+
return { s: r.status, d: d };
|
|
113
|
+
});
|
|
114
|
+
})
|
|
115
|
+
.then(function (res) {
|
|
116
|
+
if (res.s === 429) {
|
|
117
|
+
say('Too many attempts. Try again later.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!res.d.ok) {
|
|
121
|
+
say('That did not work.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
document.getElementById('dp-passcode').value = '';
|
|
125
|
+
loginEl.hidden = true;
|
|
126
|
+
panelEl.hidden = false;
|
|
127
|
+
say('');
|
|
128
|
+
return refresh();
|
|
129
|
+
})
|
|
130
|
+
.catch(function () {
|
|
131
|
+
say('That did not work.');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
document.getElementById('dp-upload').addEventListener('submit', function (e) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
var input = document.getElementById('dp-file');
|
|
138
|
+
if (!input.files || !input.files[0]) return;
|
|
139
|
+
var fd = new FormData();
|
|
140
|
+
fd.append('file', input.files[0]);
|
|
141
|
+
say('Uploading…');
|
|
142
|
+
api('/api/upload', { method: 'POST', body: fd })
|
|
143
|
+
.then(function (r) {
|
|
144
|
+
return r.json();
|
|
145
|
+
})
|
|
146
|
+
.then(function (d) {
|
|
147
|
+
say(d.ok ? 'Uploaded.' : 'Upload failed.');
|
|
148
|
+
input.value = '';
|
|
149
|
+
return refresh();
|
|
150
|
+
})
|
|
151
|
+
.catch(function () {
|
|
152
|
+
say('Upload failed.');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
})();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- D1 schema for the data portal (data-portal skill).
|
|
2
|
+
--
|
|
3
|
+
-- Four tables: who may authenticate (people), who currently is (sessions),
|
|
4
|
+
-- what they have dropped (manifest), and how hard they have been trying
|
|
5
|
+
-- (auth_attempts). Everything is IF NOT EXISTS so re-applying is safe.
|
|
6
|
+
|
|
7
|
+
-- One row per enrolled person. No plaintext passcode is ever stored: the skill
|
|
8
|
+
-- generates the passcode, shows it to the operator once, and persists only the
|
|
9
|
+
-- PBKDF2 salt and hash. `iterations` is stored per row so the cost can be
|
|
10
|
+
-- raised later without invalidating existing rows.
|
|
11
|
+
CREATE TABLE IF NOT EXISTS people (
|
|
12
|
+
ownerId TEXT PRIMARY KEY,
|
|
13
|
+
name TEXT NOT NULL,
|
|
14
|
+
salt TEXT NOT NULL,
|
|
15
|
+
hash TEXT NOT NULL,
|
|
16
|
+
iterations INTEGER NOT NULL,
|
|
17
|
+
createdAt TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- `pcCheck` is a short digest of the person's passcode hash at mint time, and
|
|
21
|
+
-- resolveSession requires it to still match `people.hash`. It is what makes
|
|
22
|
+
-- revocation structural: rotating a passcode necessarily changes the hash, so
|
|
23
|
+
-- every session minted against the old one stops resolving immediately, with
|
|
24
|
+
-- no cleanup step for an operator to remember and no 12-hour window to wait
|
|
25
|
+
-- out. A leaked passcode is exactly the case where a procedural remedy fails.
|
|
26
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
27
|
+
sessionId TEXT PRIMARY KEY,
|
|
28
|
+
ownerId TEXT NOT NULL,
|
|
29
|
+
pcCheck TEXT NOT NULL,
|
|
30
|
+
expiresAt INTEGER NOT NULL,
|
|
31
|
+
createdAt TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS sessions_owner ON sessions (ownerId);
|
|
35
|
+
|
|
36
|
+
-- One row per stored object. `ingested` is the claim flag the device-side
|
|
37
|
+
-- sweep (a separate task) reads and sets; a row stays 0 until its file has
|
|
38
|
+
-- been pulled into the graph, so the backlog survives the device being offline.
|
|
39
|
+
CREATE TABLE IF NOT EXISTS manifest (
|
|
40
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
+
fileId TEXT UNIQUE NOT NULL,
|
|
42
|
+
ownerId TEXT NOT NULL,
|
|
43
|
+
filename TEXT NOT NULL,
|
|
44
|
+
objectKey TEXT UNIQUE NOT NULL,
|
|
45
|
+
size INTEGER NOT NULL,
|
|
46
|
+
uploadedAt TEXT NOT NULL,
|
|
47
|
+
ingested INTEGER NOT NULL DEFAULT 0
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS manifest_uningested ON manifest (ingested);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS manifest_owner ON manifest (ownerId);
|
|
52
|
+
|
|
53
|
+
-- Fixed-window auth attempt counter. Pages Functions are stateless, so the
|
|
54
|
+
-- counter cannot live in process memory the way every other rate limiter in
|
|
55
|
+
-- this platform does. The unique index is what makes the upsert in
|
|
56
|
+
-- _lib/ratelimit.ts collapse onto one row per window.
|
|
57
|
+
CREATE TABLE IF NOT EXISTS auth_attempts (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
scope TEXT NOT NULL,
|
|
60
|
+
windowStart INTEGER NOT NULL,
|
|
61
|
+
count INTEGER NOT NULL
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE UNIQUE INDEX IF NOT EXISTS auth_attempts_scope_window ON auth_attempts (scope, windowStart);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Cloudflare Pages config for the data portal (data-portal skill).
|
|
2
|
+
# The skill fills the placeholders at assemble time:
|
|
3
|
+
# __PROJECT_NAME__ the Pages project name
|
|
4
|
+
# __D1_DATABASE_NAME__ the D1 database name (<accountId>-<purpose>)
|
|
5
|
+
# __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
|
|
6
|
+
# __R2_BUCKET_NAME__ the R2 bucket name (<accountId>-<purpose>)
|
|
7
|
+
name = "__PROJECT_NAME__"
|
|
8
|
+
pages_build_output_dir = "public"
|
|
9
|
+
compatibility_date = "2024-01-01"
|
|
10
|
+
compatibility_flags = ["nodejs_compat"]
|
|
11
|
+
|
|
12
|
+
[[d1_databases]]
|
|
13
|
+
binding = "DB"
|
|
14
|
+
database_name = "__D1_DATABASE_NAME__"
|
|
15
|
+
database_id = "__D1_DATABASE_ID__"
|
|
16
|
+
|
|
17
|
+
# R2 binds by NAME and has no database_id analogue — the one asymmetry with the
|
|
18
|
+
# D1 block above. `binding` is the name the Pages Function reads off env.
|
|
19
|
+
[[r2_buckets]]
|
|
20
|
+
binding = "BUCKET"
|
|
21
|
+
bucket_name = "__R2_BUCKET_NAME__"
|
|
@@ -61,6 +61,14 @@ When the canonical tree carries a contact or waitlist form, the deploy is not co
|
|
|
61
61
|
|
|
62
62
|
Name the database with the canonical `<accountId>-<purpose>` scheme from `bin/cf-store-name.sh`, and keep one store per capture purpose: a new capture kind is a discriminator column, not a second database. A document-acceptance form (works agreement, quote signing) uses the canonical `acceptances` shape declared in `references/d1-data-capture.md`, not a per-build table, so the watcher reads one known store.
|
|
63
63
|
|
|
64
|
+
## Bind the bucket when the tree binds one
|
|
65
|
+
|
|
66
|
+
When the canonical tree carries an `[[r2_buckets]]` block, the deploy is not complete until a Function can reach the bucket off `env`. Follow `references/r2-object-storage.md`: the bucket is created once by its owning account through the storage broker — not here, and not by this skill's token — the binding is added to `wrangler.toml`, and the Function reads `env.<binding>`.
|
|
67
|
+
|
|
68
|
+
The binding is declarative config carried in the deploy payload, so the Pages token from the auth step already covers it and no R2 scope is needed at deploy time. Only bucket *creation* needs `Workers R2 Storage Write`, which is the `storage` scope, and it happens outside this skill. An R2-bound Function that fails every store call with `result=failed` is the signature of a binding that did not deploy — check the block reached the tree with `bucket_name` filled, rather than reaching for a token the deploy never needed.
|
|
69
|
+
|
|
70
|
+
Name the bucket with the canonical `<accountId>-<purpose>` scheme from `bin/cf-store-name.sh`, and keep one bucket per purpose, exactly as the D1 section above requires for databases. A tree with no `[[r2_buckets]]` block skips this section.
|
|
71
|
+
|
|
64
72
|
## Open Graph correctness
|
|
65
73
|
|
|
66
74
|
The live page must carry brand-correct Open Graph metadata: `<meta property="og:title">`, `og:description`, `og:image`, and `og:url` reflecting this site and this brand, not a leftover from whatever the tree was cloned from. Stale OG (a live card still naming a previous brand) is a recorded failure mode of hand deployment. Confirm the `og:*` tags in the deployed HTML are correct before claiming done; if they are stale, fix them in the source tree, redeploy, and re-check.
|