diff --git a/agent-skills/use-zpan/SKILL.md b/agent-skills/use-zpan/SKILL.md new file mode 100644 index 00000000..bb987c9b --- /dev/null +++ b/agent-skills/use-zpan/SKILL.md @@ -0,0 +1,169 @@ +--- +name: use-zpan +description: Use ZPan through Realmroot to browse private workspaces; list, inspect, upload, download, rename, move, copy, delete, restore, or purge files and folders; create or revoke shares; inspect quota and storage usage; and handle workspace-scoped access or upload-capacity workflows. Use whenever an Agent needs to read or operate a user's private ZPan storage with controller-approved least-privilege access. +--- + +# Use ZPan + +Treat ZPan as private workspace-scoped file storage. Operate it through the +stable Agent identity and authority supplied by `$realmroot`. Do not borrow the +user's browser session, cookies, OAuth tokens, API keys, or WebDAV credentials. + +## Discover The Live Contract + +Require `$realmroot` to be installed. Reuse a known healthy ZPan Resource +Server, Context, operation, and authority. Discover or refresh only when the +target, Context, operation, or required scope is unknown, or after a connection, +authorization, or contract failure: + +```bash +realmroot toolbox +realmroot toolbox zpan +realmroot toolbox zpan --search "" +realmroot toolbox zpan --help +``` + +Select only the Resource Server named `zpan` unless the user explicitly asks +for another discovered deployment such as staging. Require its protected +resource URL to match the intended deployment. Treat Toolbox output and the +operation's live help as authoritative for operation names, arguments, scopes, +and response shapes; examples in this Skill are not a substitute for discovery. + +## Select The Workspace Context + +Inspect Contexts when no default is selected, multiple workspaces could match, +or the user names a workspace: + +```bash +realmroot toolbox zpan context +realmroot toolbox zpan context show "" +``` + +Use `--context ""` for one workflow. Change the default with +`context use` only when the user asks to change it. A ZPan target credential is +bound to exactly one workspace; never add a custom workspace header or assume +that an object ID selects the correct workspace. + +If the Context exists but is not connected or authorized, use `$realmroot` to +request the connection or authority and wait for controller approval. Do not +switch to the user's identity. When two workspaces are involved, finish and +verify operations in one Context before switching to the other. + +## Request Least-Privilege Authority + +Inspect every operation needed for the current workflow, then request their +scope union in one controller approval. Omit unrelated scopes. Typical mappings +are: + +- list, inspect, and download objects or inspect trash: `objects:read`; +- create folders and upload or save objects: `objects:create`; +- rename, move, copy, restore, or transfer objects: `objects:update`; +- move objects to trash: `objects:delete`; +- permanently remove trashed objects: `objects:purge`; +- list, create, or revoke shares: `shares:read`, `shares:create`, or + `shares:delete`; +- inspect quota or storage usage: `quota:read` or `storage-usage:read`. + +Request all required scopes together and bind the request to the selected +Context: + +```bash +realmroot agent request \ + --resource-server zpan \ + --scope \ + --context "" \ + --reason "Manage the requested ZPan files" +``` + +Repeat `--scope` for each required scope. Existing broader durable authority +may satisfy a narrower request, but each issued target credential must contain +only the scopes needed now. + +## Resolve Objects Before Acting + +List or search within the selected Context and use IDs returned by ZPan: + +```bash +realmroot toolbox zpan objects list-objects --parent "" --json +realmroot toolbox zpan objects list-objects --search "" --json +realmroot toolbox zpan objects get-object "" --json +``` + +Follow `nextPageToken` until the desired object is found or results are +exhausted. Do not guess IDs, treat a same-named object in another Context as the +target, or use a stale ID from another deployment. If multiple objects match +and the user's intent does not distinguish them, present the material choices. + +## Upload Files Directly + +Inspect `create-object` help, determine the local file's exact byte size and +media type, and create a draft with `objects:create`. Preserve the exact create +body when retrying after an uncertain result or capacity purchase. + +Use the returned `upload` descriptor as the authoritative workflow: + +1. For every `upload.parts[]` entry, read exactly `offset` through + `offset + length` from the local file. +2. Send that byte range directly to the part's presigned `url` with its exact + method and `headers`. Do not route file bytes through Realmroot or ZPan. +3. Require a successful storage response and retain the response `ETag` with + its `partNumber`. +4. Call the descriptor's `workflow.complete.operationId` with every + `{partNumber, etag}` pair. +5. Read the completed object back and confirm its name, size, status, and + selected workspace. + +Use the advertised re-presign operation only for missing or expired part URLs. +Abort the advertised upload session when the workflow cannot be completed and +the user does not want the draft retained. Never log, display, or persist +presigned URLs; they are short-lived bearer capabilities. + +For folders, create an object with the live folder representation advertised by +`create-object`; no storage upload follows. For a directory tree, create parent +folders before children and preserve relative paths. + +## Download And Verify Files + +Call `get-object` in the selected Context and use its current `downloadUrl` +immediately. Download directly from that presigned URL without sending it +through Realmroot. Do not persist or reveal the URL. + +When the user asks to copy or verify a file, compare byte length and a local +cryptographic digest when practical. A successful HTTP status alone does not +prove that the intended bytes were received. + +## Mutate, Share, And Remove Objects + +Discover the relevant live operation first. After rename, move, copy, restore, +transfer, or share changes, read the object or share back when possible and +verify the requested state. + +Treat `delete-object` as a soft delete into trash. Use permanent purge only +when the user explicitly requests irreversible removal, resolve the current +trash object first, and verify its absence afterward. Revoke shares before +cleanup when the workflow created them. Share tokens, passwords, direct URLs, +and recipient details are sensitive; return them only as needed for the user's +request. + +## Handle Capacity And Failures + +- On `402 CAPACITY_REQUIRED`, preserve the original create body and request + hash. Follow only the purchase operation and offer advertised by the live + response. Use `$realmroot` for any discovered payer and controller-approved + budget, then retry the unchanged create request after confirmed delivery. +- On `401`, stop and use `$realmroot` to refresh the connection or target + credential. Never retry with copied credentials. +- On `403`, re-read operation help and request only the missing task scope; also + respect the workspace role reported by ZPan. +- On `404`, verify the deployment, Context, and current object ID. Do not assume + that cross-workspace invisibility means deletion. +- On `409`, honor the user's conflict intent and use only a conflict strategy + advertised by the live schema. +- On expired upload or download URLs, request fresh descriptors instead of + reconstructing URLs. +- On an uncertain mutation result, read current state before retrying to avoid + duplicate folders, drafts, copies, or shares. + +Do not stop after discovery or approval. Complete the requested ZPan operation, +verify the resulting state, and report the selected workspace plus the relevant +object or share result without exposing credentials or presigned URLs. diff --git a/agent-skills/use-zpan/agents/openai.yaml b/agent-skills/use-zpan/agents/openai.yaml new file mode 100644 index 00000000..0416346a --- /dev/null +++ b/agent-skills/use-zpan/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Use ZPan" + short_description: "Manage private ZPan files through Realmroot" + default_prompt: "Use $use-zpan to manage my private ZPan files through Realmroot." diff --git a/docs/design/oauth-server.md b/docs/design/oauth-server.md index 5a8050f0..1055d636 100644 --- a/docs/design/oauth-server.md +++ b/docs/design/oauth-server.md @@ -29,6 +29,8 @@ Starting from `https://zpan.example/api`, clients discover: | API discovery | `/api` | | OpenAPI | `/api/openapi.json` | | Upload workflows | `/api/workflows.arazzo.json` | +| Agent Skills Discovery | `/.well-known/agent-skills/index.json` | +| Realmroot-oriented ZPan Skill | `/.well-known/agent-skills/use-zpan.tar.gz` | | Protected resource metadata | `/.well-known/oauth-protected-resource/api` | | Authorization server metadata | `/.well-known/oauth-authorization-server/api/auth` | | Dynamic client registration | `/api/auth/oauth2/register` | @@ -74,6 +76,14 @@ an automatic consequence of installing or updating a plugin. ZPan separately publishes its dynamic-registration and RFC 7592 configuration operations implemented at the auth boundary. +The optional `use-zpan` Agent Skill teaches Realmroot Toolbox orchestration, +workspace Context selection, least-privilege scope requests, and safe direct +upload/download handling. It treats live Toolbox help, OpenAPI, Arazzo, and +response-provided descriptors as authoritative; it does not embed credentials, +deployment URLs, or a parallel API contract. Generic controllers remain able to +operate ZPan without installing the Skill. Discovery and the archive are +generated into the static asset tree at build time; they are not Worker routes. + ## Workspace Authorization Details ZPan defines this RFC 9396 authorization detail type: diff --git a/docs/roadmap/v2.9.md b/docs/roadmap/v2.9.md index 3c6324b7..32144050 100644 --- a/docs/roadmap/v2.9.md +++ b/docs/roadmap/v2.9.md @@ -17,7 +17,9 @@ ZPan owns: - JWT bearer actor authentication and OAuth token exchange; - DPoP-bound resource tokens and revocation; - scope-aware file APIs, Arazzo workflows, and structured direct-upload - instructions. + instructions; +- Agent Skills Discovery and an optional Realmroot-oriented workflow Skill + that consumes the live self-describing contract. The external controller owns Agent identity, approval of application access, delegated credential injection, and tool orchestration. ZPan does not ship a fixed OAuth @@ -89,11 +91,23 @@ draft with part numbers and ETags. Re-presigning and completion reauthorize against ZPan. Presigned URLs and file bytes never need to transit the external controller. +### Optional Agent Skill + +- Publish Agent Skills Discovery v0.2.0 at the Resource Server origin. +- Publish `use-zpan` as a digest-pinned archive that requires `$realmroot` and + teaches workspace Context selection, least-privilege access, file lifecycle, + and safe direct transfer handling. +- Keep live Toolbox operation help, OpenAPI, Arazzo, and response-provided + descriptors authoritative; do not duplicate deployment URLs, credentials, or + the complete API schema in the Skill. +- Preserve the protocol goal: generic controllers can operate ZPan without the + Skill. + ### Authorization -The grantable resource scopes cover object read/create/update/delete, share -read/create/delete, quota read, storage-usage read, and task read. Purge, -administration, billing, credential management, WebDAV configuration, and +The grantable resource scopes cover object read/create/update/delete/purge, +share read/create/delete, quota read, storage-usage read, and task read. +Administration, billing, credential management, WebDAV configuration, and downloader registration remain excluded. OAuth resolves to the same protocol-neutral principal and route policies used @@ -105,7 +119,8 @@ controller or client ID. - fixed, system-managed OAuth client; - OpenAPI `x-cli-config` profiles; - `restish-zpan` command plugin and release artifact; -- repository-hosted ZPan Agent skill. +- the legacy credential-coupled ZPan Agent skill that duplicated target routes + and profiles. The existing `zpan-cli` device authorization remains only for its legacy, single-use downloader-registration bootstrap. @@ -119,8 +134,9 @@ single-use downloader-registration bootstrap. 5. Tool-neutral OpenAPI authorization metadata. 6. Discoverable Arazzo upload workflows. 7. Self-describing single/multipart upload responses. -8. Remove fixed-client, profile, plugin, and skill surfaces. -9. Complete local gates and real FlareAuth acceptance. +8. Remove fixed-client, profile, plugin, and credential-coupled skill surfaces. +9. Publish the optional discovery-based `use-zpan` Skill. +10. Complete local gates and real FlareAuth acceptance. ## Acceptance Criteria @@ -132,6 +148,8 @@ single-use downloader-registration bootstrap. - After approval, the controller obtains a DPoP resource token for `/api`. - A generic Agent creates an upload, sends all returned byte ranges, captures ETags, and completes it without ZPan-specific code. +- Realmroot discovers the optional `use-zpan` Skill, verifies its digest, and + presents an installation command. - The same connection lists, reads, and renames the uploaded file. - Grant or JWT revocation stops subsequent resource access. - Lint, type checking, Node tests, Cloudflare tests, and applicable end-to-end diff --git a/package.json b/package.json index ef99ed2b..34e8e7a9 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "scripts": { "dev": "CLOUDFLARE_ENV=staging vite dev", "dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node", - "build": "[ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"main\" ] && export CLOUDFLARE_ENV=staging; vite build", - "build:node": "vite build --mode node && tsup server/entry-node.ts --config server/tsup.config.mjs --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client", - "build:lambda": "tsup server/entry-lambda.ts --config server/tsup.config.mjs --format cjs --outDir dist-lambda --external @libsql/client", - "build:vercel": "vite build --mode node && tsup server/entry-vercel.ts --config server/tsup.config.mjs --format esm --outDir api --external @libsql/client", - "build:netlify": "tsup server/entry-netlify.ts --config server/tsup.config.mjs --format esm --outDir netlify/functions --external @libsql/client", - "build:azure": "vite build && tsup server/entry-azure.ts --config server/tsup.config.mjs --format esm --outDir azure-functions --external @azure/functions --external @libsql/client && cp server/azure-host.json azure-functions/host.json && cp -r dist azure-functions/dist", + "agent-skills:build": "node scripts/build-agent-skills.mjs", + "agent-skills:check": "node scripts/build-agent-skills.mjs --check", + "build": "pnpm agent-skills:build && (if [ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"main\" ]; then export CLOUDFLARE_ENV=staging; fi; vite build)", + "build:node": "pnpm agent-skills:build && vite build --mode node && tsup server/entry-node.ts --config server/tsup.config.mjs --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client", + "build:lambda": "pnpm agent-skills:build && tsup server/entry-lambda.ts --config server/tsup.config.mjs --format cjs --outDir dist-lambda --external @libsql/client", + "build:vercel": "pnpm agent-skills:build && vite build --mode node && tsup server/entry-vercel.ts --config server/tsup.config.mjs --format esm --outDir api --external @libsql/client", + "build:netlify": "pnpm agent-skills:build && tsup server/entry-netlify.ts --config server/tsup.config.mjs --format esm --outDir netlify/functions --external @libsql/client", + "build:azure": "pnpm agent-skills:build && vite build && tsup server/entry-azure.ts --config server/tsup.config.mjs --format esm --outDir azure-functions --external @azure/functions --external @libsql/client && cp server/azure-host.json azure-functions/host.json && cp -r dist azure-functions/dist", "deploy": "pnpm build && pnpm db:migrate:d1:prod && wrangler deploy", "release": "node scripts/release.mjs", "release:check-version": "node scripts/check-release-version.mjs", @@ -36,7 +38,7 @@ "test:cf": "vitest run --project cloudflare-contract --project cloudflare-isolated", "test:libsql": "vitest run --project libsql", "test:watch": "vitest --project backend-unit --project frontend-unit --project backend-integration-http --project backend-integration-data", - "lint": "biome check . && pnpm lint:ids && pnpm lint:tests", + "lint": "biome check . && pnpm agent-skills:check && pnpm lint:ids && pnpm lint:tests", "lint:ids": "node scripts/lint-id-generation.mjs", "lint:tests": "node scripts/lint-test-boundaries.mjs", "lint:fix": "biome check --write .", diff --git a/public/.well-known/agent-skills/index.json b/public/.well-known/agent-skills/index.json new file mode 100644 index 00000000..394ee3b3 --- /dev/null +++ b/public/.well-known/agent-skills/index.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "use-zpan", + "type": "archive", + "description": "Use ZPan through Realmroot to browse private workspaces; list, inspect, upload, download, rename, move, copy, delete, restore, or purge files and folders; create or revoke shares; inspect quota and storage usage; and handle workspace-scoped access or upload-capacity workflows. Use whenever an Agent needs to read or operate a user's private ZPan storage with controller-approved least-privilege access.", + "url": "/.well-known/agent-skills/use-zpan.tar.gz", + "digest": "sha256:0c3a46eaa73ff9488fa6de137b0d0082ae8e43d39253c676def0aba41d5fce01" + } + ] +} diff --git a/public/.well-known/agent-skills/use-zpan.tar.gz b/public/.well-known/agent-skills/use-zpan.tar.gz new file mode 100644 index 00000000..7311657e Binary files /dev/null and b/public/.well-known/agent-skills/use-zpan.tar.gz differ diff --git a/scripts/build-agent-skills.mjs b/scripts/build-agent-skills.mjs new file mode 100644 index 00000000..477026e2 --- /dev/null +++ b/scripts/build-agent-skills.mjs @@ -0,0 +1,100 @@ +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { gzipSync } from 'node:zlib' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const skillDirectory = join(root, 'agent-skills', 'use-zpan') +const outputDirectory = join(root, 'public', '.well-known', 'agent-skills') +const archivePath = join(outputDirectory, 'use-zpan.tar.gz') +const indexPath = join(outputDirectory, 'index.json') +const files = ['SKILL.md', 'agents/openai.yaml'].map((name) => ({ + name, + content: readFileSync(join(skillDirectory, name)), +})) + +const frontmatter = files[0].content.toString('utf8').match(/^---\n([\s\S]*?)\n---/) +if (!frontmatter) throw new Error('use-zpan SKILL.md has no YAML frontmatter') + +const name = frontmatter[1].match(/^name:\s*(.+)$/m)?.[1]?.trim() +const description = frontmatter[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() +if (name !== 'use-zpan' || !description) throw new Error('use-zpan SKILL.md has invalid metadata') + +const archive = gzipSync(createTar(files), { level: 9, mtime: 0 }) +archive[9] = 255 +const digest = `sha256:${createHash('sha256').update(archive).digest('hex')}` +const index = `${JSON.stringify( + { + $schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json', + skills: [ + { + name, + type: 'archive', + description, + url: '/.well-known/agent-skills/use-zpan.tar.gz', + digest, + }, + ], + }, + null, + 2, +)}\n` + +mkdirSync(outputDirectory, { recursive: true }) +if (process.argv.includes('--check')) { + if ( + !existsSync(indexPath) || + !existsSync(archivePath) || + readFileSync(indexPath, 'utf8') !== index || + !readFileSync(archivePath).equals(archive) + ) { + throw new Error('Generated use-zpan Skill artifact is stale; run pnpm agent-skills:build') + } +} else { + writeFileSync(indexPath, index) + writeFileSync(archivePath, archive) +} + +function createTar(entries) { + const blocks = [] + for (const entry of entries) { + const header = Buffer.alloc(512) + writeString(header, entry.name, 0, 100) + writeOctal(header, 0o644, 100, 8) + writeOctal(header, 0, 108, 8) + writeOctal(header, 0, 116, 8) + writeOctal(header, entry.content.length, 124, 12) + writeOctal(header, 0, 136, 12) + header.fill(0x20, 148, 156) + header[156] = 0x30 + writeString(header, 'ustar', 257, 6) + writeString(header, '00', 263, 2) + writeString(header, 'zpan', 265, 32) + writeString(header, 'zpan', 297, 32) + writeOctal(header, checksum(header), 148, 8) + blocks.push(header, entry.content) + + const padding = (512 - (entry.content.length % 512)) % 512 + if (padding > 0) blocks.push(Buffer.alloc(padding)) + } + blocks.push(Buffer.alloc(1024)) + return Buffer.concat(blocks) +} + +function writeString(buffer, value, offset, length) { + const bytes = Buffer.from(value) + if (bytes.length > length) throw new Error(`Tar field is too long: ${value}`) + bytes.copy(buffer, offset) +} + +function writeOctal(buffer, value, offset, length) { + const encoded = value.toString(8).padStart(length - 1, '0') + writeString(buffer, `${encoded}\0`, offset, length) +} + +function checksum(buffer) { + let total = 0 + for (const byte of buffer) total += byte + return total +} diff --git a/scripts/build-agent-skills.test.mjs b/scripts/build-agent-skills.test.mjs new file mode 100644 index 00000000..24bdf7aa --- /dev/null +++ b/scripts/build-agent-skills.test.mjs @@ -0,0 +1,60 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { gunzipSync } from 'node:zlib' +import { describe, expect, it } from 'vitest' + +const skillDirectory = new URL('../agent-skills/use-zpan/', import.meta.url) +const outputDirectory = new URL('../public/.well-known/agent-skills/', import.meta.url) +const wranglerConfig = readFileSync(new URL('../wrangler.toml', import.meta.url), 'utf8') + +describe('Agent Skill artifacts', () => { + it('publishes a digest-pinned archive containing the canonical use-zpan Skill', () => { + const index = JSON.parse(readFileSync(new URL('index.json', outputDirectory), 'utf8')) + const archive = readFileSync(new URL('use-zpan.tar.gz', outputDirectory)) + + expect(archive[9]).toBe(255) + expect(index).toEqual({ + $schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json', + skills: [ + { + name: 'use-zpan', + type: 'archive', + description: expect.stringContaining('through Realmroot'), + url: '/.well-known/agent-skills/use-zpan.tar.gz', + digest: `sha256:${createHash('sha256').update(archive).digest('hex')}`, + }, + ], + }) + + const files = readTarFiles(gunzipSync(archive)) + expect([...files.keys()]).toEqual(['SKILL.md', 'agents/openai.yaml']) + expect(files.get('SKILL.md')).toEqual(readFileSync(new URL('SKILL.md', skillDirectory))) + expect(files.get('agents/openai.yaml')).toEqual( + readFileSync(new URL('agents/openai.yaml', skillDirectory)), + ) + }) + + it('keeps Agent Skill discovery on static assets', () => { + expect(wranglerConfig).toContain('"/.well-known/oauth-authorization-server/*"') + expect(wranglerConfig).toContain('"/.well-known/openid-configuration/*"') + expect(wranglerConfig).toContain('"/.well-known/oauth-protected-resource/*"') + expect(wranglerConfig).toContain('"/.well-known/zpan-domain-verification/*"') + expect(wranglerConfig).not.toContain('"/.well-known/*"') + expect(wranglerConfig).not.toContain('"/.well-known/agent-skills') + expect(wranglerConfig).not.toMatch(/"!\/\.well-known\//) + }) +}) + +function readTarFiles(archive) { + const files = new Map() + for (let offset = 0; offset + 512 <= archive.length; ) { + const header = archive.subarray(offset, offset + 512) + if (header.every((byte) => byte === 0)) break + const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '') + const size = Number.parseInt(header.subarray(124, 136).toString('ascii').replace(/\0.*$/, '').trim(), 8) + offset += 512 + files.set(name, archive.subarray(offset, offset + size)) + offset += Math.ceil(size / 512) * 512 + } + return files +} diff --git a/server/entry-lambda.ts b/server/entry-lambda.ts index d439d479..3814b9ea 100644 --- a/server/entry-lambda.ts +++ b/server/entry-lambda.ts @@ -12,6 +12,7 @@ const MIME: Record = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.css': 'text/css', + '.gz': 'application/gzip', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', diff --git a/vite.config.ts b/vite.config.ts index 9d8b53fe..be6a041d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,7 @@ import { resolveAppCommit, resolveAppVersion } from './scripts/app-version.mjs' const appPort = Number(process.env.E2E_APP_PORT ?? 5185) const apiPort = Number(process.env.E2E_API_PORT ?? 8222) +const nodeApiProxy = { target: `http://localhost:${apiPort}`, changeOrigin: false } const appVersion = resolveAppVersion() const appCommit = resolveAppCommit() const configuredDevHosts = (process.env.ZPAN_DEV_ALLOWED_HOSTS ?? '') @@ -91,14 +92,11 @@ export default defineConfig(({ mode }) => ({ ...(mode === 'node' ? { proxy: { - '/api': { - target: `http://localhost:${apiPort}`, - changeOrigin: false, - }, - '/.well-known': { - target: `http://localhost:${apiPort}`, - changeOrigin: false, - }, + '/api': nodeApiProxy, + '^/\\.well-known/oauth-authorization-server/': nodeApiProxy, + '^/\\.well-known/openid-configuration/': nodeApiProxy, + '^/\\.well-known/oauth-protected-resource/': nodeApiProxy, + '^/\\.well-known/zpan-domain-verification/': nodeApiProxy, }, } : {}), diff --git a/wrangler.toml b/wrangler.toml index 917b76d3..9907ac7e 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,7 +6,19 @@ compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"] [assets] binding = "ASSETS" not_found_handling = "single-page-application" -run_worker_first = ["/api", "/api/*", "/.well-known/*", "/dav", "/dav/*", "/ih/*", "/r/*", "/s/*"] +run_worker_first = [ + "/api", + "/api/*", + "/.well-known/oauth-authorization-server/*", + "/.well-known/openid-configuration/*", + "/.well-known/oauth-protected-resource/*", + "/.well-known/zpan-domain-verification/*", + "/dav", + "/dav/*", + "/ih/*", + "/r/*", + "/s/*", +] [[d1_databases]] binding = "DB"