@opendatalabs/vana-sdk 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/dist/errors.cjs +94 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +123 -0
- package/dist/errors.js +82 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.js +1155 -14
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +1205 -15
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +4 -0
- package/dist/index.node.js +1155 -14
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/gateway.cjs +16 -2
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +2 -0
- package/dist/protocol/gateway.js +16 -2
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/lineage.cjs +287 -0
- package/dist/protocol/lineage.cjs.map +1 -0
- package/dist/protocol/lineage.d.ts +228 -0
- package/dist/protocol/lineage.js +258 -0
- package/dist/protocol/lineage.js.map +1 -0
- package/dist/protocol/lineage.test.d.ts +1 -0
- package/dist/protocol/personal-server-error-body.cjs +57 -0
- package/dist/protocol/personal-server-error-body.cjs.map +1 -0
- package/dist/protocol/personal-server-error-body.d.ts +18 -0
- package/dist/protocol/personal-server-error-body.js +32 -0
- package/dist/protocol/personal-server-error-body.js.map +1 -0
- package/dist/protocol/personal-server-write.cjs +623 -0
- package/dist/protocol/personal-server-write.cjs.map +1 -0
- package/dist/protocol/personal-server-write.d.ts +284 -0
- package/dist/protocol/personal-server-write.js +601 -0
- package/dist/protocol/personal-server-write.js.map +1 -0
- package/dist/protocol/personal-server-write.test.d.ts +1 -0
- package/dist/protocol/scope-actions.cjs +185 -0
- package/dist/protocol/scope-actions.cjs.map +1 -0
- package/dist/protocol/scope-actions.d.ts +145 -0
- package/dist/protocol/scope-actions.js +154 -0
- package/dist/protocol/scope-actions.js.map +1 -0
- package/dist/protocol/scope-actions.test.d.ts +1 -0
- package/dist/protocol/write-signer.cjs +67 -0
- package/dist/protocol/write-signer.cjs.map +1 -0
- package/dist/protocol/write-signer.d.ts +59 -0
- package/dist/protocol/write-signer.js +43 -0
- package/dist/protocol/write-signer.js.map +1 -0
- package/dist/protocol/write-signer.test.d.ts +1 -0
- package/dist/tests/mock-personal-server.d.ts +127 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/personal-server-write.ts"],"sourcesContent":["/**\n * Builder-side client for the Personal Server Write API.\n *\n * @remarks\n * A builder holding a write-grant (a grant whose scope entries carry the\n * `write:` prefix, see {@link formatScopeEntry}) writes into a user's Personal\n * Server in two steps:\n *\n * 1. {@link openWriteSession}: `POST /v1/write/session` with a Web3Signed\n * handshake that carries the grant id as a signed claim. The Personal\n * Server verifies the builder key against the grant and mints a\n * short-lived bearer token bound to `{ builder, grantId }`.\n * 2. {@link writeData}: `POST /v1/data/:scope` with that bearer plus an\n * `X-Vana-Write-Signature` proof, a second Web3Signed signature over the\n * representation the Personal Server will store, again carrying the grant\n * id as a signed claim. The Personal Server stores the proof with the\n * record under the reserved `$writtenBy` key so anyone holding the record\n * can verify who wrote it.\n *\n * What the proof covers: a JSON write signs the request body, which must be\n * the compact `JSON.stringify` form (the server rejects anything else with\n * `WRITE_BODY_NOT_CANONICAL`); a binary write signs the `$binary` record the\n * server stores for the bytes and their representation headers\n * ({@link binaryWriteSignedBytes}), not the raw bytes. Every proof is\n * single-use: a retry after a lost response signs a fresh proof.\n *\n * Derivatives name their sources through `lineage` (see\n * {@link deriveDataPointId}): for a JSON write the ids are the top-level\n * `lineage` field of the body (inside the signed bytes); for a binary write\n * they are the `lineage` field of the `X-Vana-Metadata` JSON (inside the\n * signed `$binary` record). The server validates them and mirrors them under\n * the reserved `$lineage` key. Callers never send `$writtenBy` or `$lineage`.\n *\n * @category Protocol\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex, isAddress, type Address, type Hex } from \"viem\";\nimport { z } from \"zod\";\nimport { buildWeb3SignedHeader } from \"../auth/web3-signed-builder\";\nimport {\n type PersonalServerWriteError,\n WriteConflictError,\n WriteForbiddenError,\n WriteLineageError,\n WriteRejectedError,\n WriteRequestError,\n WriteSessionError,\n WriteSessionExpiredError,\n WriteTransportError,\n WriteUnauthorizedError,\n} from \"../errors\";\nimport { toBase64 } from \"../utils/encoding\";\nimport { IngestResponseSchema, type IngestResponse } from \"./data-file\";\nimport {\n assertDerivedScopeNaming,\n deriveDataPointId,\n isDataPointId,\n} from \"./lineage\";\nimport {\n isRecord,\n readPersonalServerErrorBody,\n} from \"./personal-server-error-body\";\nimport { scopeMatchesPattern } from \"./scopes\";\nimport {\n resolveWriteSigner,\n type ResolveWriteSignerOptions,\n type WriteSigner,\n type WriteSignerSource,\n} from \"./write-signer\";\n\n/** Path of the write-session handshake. */\nexport const WRITE_SESSION_PATH = \"/v1/write/session\";\n/** Header carrying the builder's per-write payload proof. */\nexport const WRITE_SIGNATURE_HEADER = \"X-Vana-Write-Signature\";\n/** Header carrying caller metadata (and `lineage`) for a binary write. */\nexport const WRITE_METADATA_HEADER = \"X-Vana-Metadata\";\n/** Field the lineage source ids travel in (body for JSON, metadata for binary). */\nexport const LINEAGE_FIELD = \"lineage\";\n/** The most sources one record may cite. */\nexport const MAX_LINEAGE_SOURCES = 256;\n/** Header carrying the filename of a binary write (printable ASCII names). */\nexport const WRITE_FILENAME_HEADER = \"X-Filename\";\n/**\n * Header carrying a filename the `X-Filename` header cannot: the Personal\n * Server percent-decodes `filename*=UTF-8''...` (RFC 5987).\n */\nexport const WRITE_CONTENT_DISPOSITION_HEADER = \"Content-Disposition\";\n/** Reserved record key the Personal Server stamps builder attribution into. */\nexport const WRITER_ATTRIBUTION_KEY = \"$writtenBy\";\n/** Reserved record key the Personal Server stamps lineage sources into. */\nexport const LINEAGE_KEY = \"$lineage\";\n/** Record keys a builder must never send. */\nexport const RESERVED_WRITE_KEYS: readonly string[] = [\n WRITER_ATTRIBUTION_KEY,\n LINEAGE_KEY,\n];\n\n/**\n * Transport-level retry knobs shared by {@link openWriteSession} and\n * {@link writeData}.\n *\n * @remarks\n * Applies only when `fetch` **throws** (connection reset, DNS, a relay drop).\n * Every attempt signs a fresh proof, because the Personal Server consumes a\n * proof the moment it accepts it. A received HTTP response is never retried:\n * a 4xx/5xx is surfaced as a typed error.\n */\nexport interface WriteTransportRetryOptions {\n /** Total attempts including the first (default 3). `1` disables retries. */\n attempts?: number;\n /** Delay before the first retry (ms); doubles per retry (default 1_000). */\n initialDelayMs?: number;\n}\n\n/** An open write session: the bearer token plus what it was minted for. */\nexport interface WriteSession {\n /** Personal Server origin, without a trailing slash. */\n personalServerUrl: string;\n /** Web3Signed audience every proof under this session is addressed to. */\n audience: string;\n /** The write-grant the session is bound to. */\n grantId: string;\n /** The bearer token (`vana_write_...`). */\n accessToken: string;\n /** Unix milliseconds after which the token is no longer accepted. */\n expiresAt: number;\n /** Write patterns (prefix stripped) the session may write into. */\n writeScopes: readonly string[];\n /** The builder key the session was opened with; signs every write proof. */\n signer: WriteSigner;\n}\n\nexport interface OpenWriteSessionParams extends ResolveWriteSignerOptions {\n /** Personal Server origin, e.g. `https://ps.example.com`. */\n personalServerUrl: string;\n /** Builder key: a viem `LocalAccount`, `WalletClient`, or `{ signMessage }`. */\n signer: WriteSignerSource;\n /** The write-grant issued to the builder. */\n grantId: string;\n /** Web3Signed audience; defaults to `personalServerUrl`. */\n audience?: string;\n /** `fetch` to use; defaults to `globalThis.fetch`. */\n fetch?: typeof fetch;\n /** Extra request headers. */\n headers?: HeadersInit;\n retry?: WriteTransportRetryOptions;\n}\n\n/** A lineage source: a data point id, or the pair it is derived from. */\nexport type LineageSource = Hex | { ownerAddress: Address; scope: string };\n\n/** Bytes to store as an unstructured (binary) record. */\nexport interface WriteBinaryPayload {\n bytes: Uint8Array;\n /** Media type, e.g. `application/pdf`. Parameters are dropped when stored. */\n contentType: string;\n /**\n * Stored with the record. Sent as `X-Filename` when it is printable ASCII,\n * otherwise as `Content-Disposition: attachment; filename*=UTF-8''...`,\n * which the Personal Server decodes back to the same string. Must not have\n * leading or trailing whitespace (HTTP would strip it and the signed\n * representation would no longer match).\n */\n filename?: string;\n}\n\ninterface WriteDataBaseParams {\n session: WriteSession;\n /**\n * The scope to write into; must match one of the session's write patterns.\n * A derived scope must not share its first dot-segment with any source's\n * scope (the server rejects it with `LINEAGE_SCOPE_UNDER_SOURCE_PREFIX`):\n * put derivatives in the app's own namespace.\n */\n scope: string;\n /**\n * The data points this record was derived from: ids\n * ({@link deriveDataPointId}) or `{ ownerAddress, scope }` pairs the SDK\n * derives the id from. Distinct, at most {@link MAX_LINEAGE_SOURCES}, all\n * belonging to the same owner as the target scope, never the record's own\n * id. Sent lowercased as the record's `lineage` field, inside the signed\n * bytes. `[]` is an explicit root statement and is sent; absent or `null`\n * makes no statement. Pairs also let the SDK apply the naming rule\n * ({@link assertDerivedScopeNaming}) before anything is signed.\n */\n lineage?: readonly LineageSource[] | null;\n /** `fetch` to use; defaults to `globalThis.fetch`. */\n fetch?: typeof fetch;\n /** Extra request headers. */\n headers?: HeadersInit;\n retry?: WriteTransportRetryOptions;\n}\n\n/**\n * A JSON write: `data` is stored as the record (plus `lineage` when given).\n * Anything else to store goes inside `data`; `lineage` and reserved keys are\n * not accepted in it.\n */\nexport interface WriteJsonDataParams extends WriteDataBaseParams {\n data: Record<string, unknown>;\n binary?: never;\n metadata?: never;\n}\n\n/** A binary write: the bytes are stored as a `$binary` record. */\nexport interface WriteBinaryDataParams extends WriteDataBaseParams {\n binary: WriteBinaryPayload;\n data?: never;\n /**\n * Caller metadata stored with the record (`X-Vana-Metadata`, part of the\n * signed `$binary` record). Must not contain `lineage` (use the option) or\n * a reserved key.\n */\n metadata?: Record<string, unknown>;\n}\n\nexport type WriteDataParams = WriteJsonDataParams | WriteBinaryDataParams;\n\nconst WriteDataResultSchema = IngestResponseSchema.extend({\n // Present when the write carried lineage: the validated, lowercased ids.\n lineage: z.object({ sources: z.array(z.string()) }).optional(),\n});\n\n/** The Personal Server's ingest answer, plus the accepted lineage if any. */\nexport type WriteDataResult = IngestResponse & {\n lineage?: { sources: Hex[] };\n};\n\nconst WriteSessionResponseSchema = z.object({\n access_token: z.string().min(1),\n token_type: z.string(),\n expires_in: z.number().nonnegative(),\n scope: z.string(),\n});\n\nfunction normalizeBaseUrl(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nfunction resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch {\n const resolved = fetchFn ?? globalThis.fetch;\n if (resolved === undefined) {\n throw new WriteRequestError(\"No fetch implementation available\");\n }\n return resolved;\n}\n\nfunction dataPath(scope: string): string {\n return `/v1/data/${encodeURIComponent(scope)}`;\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction finiteOr(value: number | undefined, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The Personal Server consumes every proof it accepts, and a Web3Signed\n * payload is fully determined by `{ aud, method, uri, bodyHash, grantId, iat,\n * exp }`, so two proofs for the same request signed within one second would\n * be byte-identical and the second rejected as a replay. Remember the\n * highest `iat` issued per request identity in this process and bump past\n * it when a second proof for the same identity falls inside the same second.\n *\n * A mark is kept for as long as the server can still remember the proof it\n * guards (its lifetime plus the verifier's clock skew), so a proof is never\n * re-issued while it could still be rejected as a replay: not by a burst,\n * and not by a wall clock stepping backwards (an identical request after a\n * step back waits for the clock instead of reusing the mark). Marks are\n * bucketed by their `iat` second, so pruning (once per second) only touches\n * the buckets that fell out of the retention window: the work per proof is\n * amortised constant and the map is bounded by the distinct requests signed\n * in the window.\n */\nconst issuedProofIats = new Map<string, number>();\n/** `iat` second -> identities whose current mark is that second. */\nconst issuedProofBuckets = new Map<number, Set<string>>();\nlet issuedProofIatsPrunedAtSec = 0;\n/** `buildWeb3SignedHeader`'s default `exp - iat`. */\nconst WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;\n/** The verifier's tolerated clock skew (`verifyWeb3Signed`). */\nconst WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;\nconst PROOF_IAT_RETENTION_SECONDS =\n WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;\n/**\n * How far ahead of the clock a bumped `iat` may run when the proof is sent.\n * The verifier tolerates 60 s of skew. A burst of identical requests that\n * would need to run further ahead waits for the clock instead, so a proof is\n * never repeated; sustained identical requests are throttled to one per\n * second after a burst of this many.\n */\nconst PROOF_IAT_MAX_AHEAD_SECONDS = 30;\n\nfunction pruneIssuedProofIats(nowSec: number): void {\n if (issuedProofIatsPrunedAtSec === nowSec) return;\n issuedProofIatsPrunedAtSec = nowSec;\n const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;\n // There is at most one bucket per second in the window, so this walk is\n // bounded by the window length, not by the number of marks.\n for (const [sec, keys] of issuedProofBuckets) {\n if (sec >= cutoff) continue;\n for (const key of keys) issuedProofIats.delete(key);\n issuedProofBuckets.delete(sec);\n }\n}\n\nfunction setIssuedProofIat(key: string, iat: number, previous?: number): void {\n if (previous !== undefined) {\n const bucket = issuedProofBuckets.get(previous);\n bucket?.delete(key);\n if (bucket?.size === 0) issuedProofBuckets.delete(previous);\n }\n issuedProofIats.set(key, iat);\n let bucket = issuedProofBuckets.get(iat);\n if (bucket === undefined) {\n bucket = new Set();\n issuedProofBuckets.set(iat, bucket);\n }\n bucket.add(key);\n}\n\n/**\n * Reserve the next `iat` for a request identity. The reservation is made\n * synchronously so concurrent callers never share a value; the returned\n * promise only waits when the reserved `iat` is further ahead of the clock\n * than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.\n */\nfunction nextProofIat(proofKey: string): Promise<number> {\n const nowSec = Math.floor(Date.now() / 1000);\n pruneIssuedProofIats(nowSec);\n const last = issuedProofIats.get(proofKey);\n const iat = last === undefined ? nowSec : Math.max(nowSec, last + 1);\n setIssuedProofIat(proofKey, iat, last);\n const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;\n if (waitSec <= 0) return Promise.resolve(iat);\n return sleep(waitSec * 1000).then(() => iat);\n}\n\nfunction proofKeyFor(parts: {\n aud: string;\n method: string;\n uri: string;\n grantId: string;\n signedBytes?: Uint8Array;\n}): string {\n // A digest, so a retained mark costs a fixed amount of memory whatever the\n // request looked like.\n return bytesToHex(\n sha256(\n new TextEncoder().encode(\n JSON.stringify([\n parts.aud,\n parts.method,\n parts.uri,\n parts.grantId,\n parts.signedBytes ? bytesToHex(sha256(parts.signedBytes)) : \"\",\n ]),\n ),\n ),\n );\n}\n\n/**\n * Send a request, re-signing it on every attempt. Only a thrown `fetch` is\n * retried; the proof builder and any received response are never retried.\n */\nasync function sendWithFreshProof(\n label: string,\n fetchFn: typeof fetch,\n options: WriteTransportRetryOptions | undefined,\n proofKey: string,\n build: (iat: number) => Promise<{ url: string; init: RequestInit }>,\n): Promise<Response> {\n const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));\n let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1_000));\n let lastError: unknown;\n for (let attempt = 0; attempt < attempts; attempt++) {\n const { url, init } = await build(await nextProofIat(proofKey));\n try {\n return await fetchFn(url, init);\n } catch (err) {\n lastError = err;\n }\n if (attempt < attempts - 1) {\n await sleep(delayMs);\n delayMs *= 2;\n }\n }\n throw new WriteTransportError(\n `${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,\n attempts,\n lastError,\n );\n}\n\n/**\n * Open a write session against a Personal Server.\n *\n * @remarks\n * Sends `POST /v1/write/session` with a Web3Signed `Authorization` header\n * whose signed claims carry `grantId`. The handshake proof is single-use on\n * the server; a transport retry signs a new one.\n *\n * @returns The session to pass to {@link writeData}.\n * @throws {WriteSessionError} When the Personal Server refuses the handshake\n * (`errorCode` names why: `UNREGISTERED_BUILDER`, `GRANT_REQUIRED`,\n * `GRANT_REVOKED`, `SCOPE_MISMATCH` for a grant without write entries,\n * `INVALID_SIGNATURE` when the key is not the grantee,\n * `GRANT_OWNER_MISMATCH`, ...).\n * @throws {WriteTransportError} When `fetch` threw on every attempt.\n * @throws {WriteRequestError} When the signer is unusable.\n */\nexport async function openWriteSession(\n params: OpenWriteSessionParams,\n): Promise<WriteSession> {\n const fetchFn = resolveFetch(params.fetch);\n const personalServerUrl = normalizeBaseUrl(params.personalServerUrl);\n const audience = params.audience ?? personalServerUrl;\n const signer = resolveWriteSigner(params.signer, { account: params.account });\n\n const response = await sendWithFreshProof(\n \"Write session handshake\",\n fetchFn,\n params.retry,\n proofKeyFor({\n aud: audience,\n method: \"POST\",\n uri: WRITE_SESSION_PATH,\n grantId: params.grantId,\n }),\n async (iat) => {\n const headers = new Headers(params.headers);\n headers.set(\n \"Authorization\",\n await buildWeb3SignedHeader({\n signMessage: signer.signMessage,\n aud: audience,\n method: \"POST\",\n uri: WRITE_SESSION_PATH,\n grantId: params.grantId,\n iat,\n }),\n );\n return {\n url: `${personalServerUrl}${WRITE_SESSION_PATH}`,\n init: { method: \"POST\", headers },\n };\n },\n );\n // Read the clock before parsing so `expiresAt` never overstates the\n // remaining lifetime.\n const mintedAt = Date.now();\n\n if (!response.ok) {\n const { errorCode, message, details } =\n await readPersonalServerErrorBody(response);\n throw new WriteSessionError(\n message ??\n `Write session handshake failed: ${response.status} ${response.statusText}`,\n response.status,\n errorCode,\n details,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch (err) {\n throw new WriteSessionError(\n \"Write session response is not JSON\",\n response.status,\n null,\n { cause: errorMessage(err) },\n );\n }\n const parsed = WriteSessionResponseSchema.safeParse(body);\n if (!parsed.success) {\n throw new WriteSessionError(\n \"Write session response is not a session\",\n response.status,\n null,\n { issues: parsed.error.issues },\n );\n }\n if (parsed.data.token_type.toLowerCase() !== \"bearer\") {\n throw new WriteSessionError(\n `Write session token type is not Bearer: ${parsed.data.token_type}`,\n response.status,\n );\n }\n\n return {\n personalServerUrl,\n audience,\n grantId: params.grantId,\n accessToken: parsed.data.access_token,\n expiresAt: mintedAt + parsed.data.expires_in * 1000,\n writeScopes: parsed.data.scope.split(\" \").filter((s) => s.length > 0),\n signer,\n };\n}\n\n/** Every field {@link writeData} relies on, checked before any is used. */\nfunction isWriteSession(value: unknown): value is WriteSession {\n if (!isRecord(value)) return false;\n const signer: unknown = value.signer;\n return (\n typeof value.personalServerUrl === \"string\" &&\n typeof value.audience === \"string\" &&\n typeof value.grantId === \"string\" &&\n typeof value.accessToken === \"string\" &&\n value.accessToken.length > 0 &&\n typeof value.expiresAt === \"number\" &&\n Number.isFinite(value.expiresAt) &&\n Array.isArray(value.writeScopes) &&\n value.writeScopes.every((scope) => typeof scope === \"string\") &&\n isRecord(signer) &&\n typeof signer.signMessage === \"function\"\n );\n}\n\n/** `true` when one of the session's write patterns covers `scope`. */\nexport function sessionCoversScope(\n session: Pick<WriteSession, \"writeScopes\">,\n scope: string,\n): boolean {\n if (\n !isRecord(session) ||\n !Array.isArray(session.writeScopes) ||\n !session.writeScopes.every((pattern) => typeof pattern === \"string\")\n ) {\n throw new WriteRequestError(\"session must come from openWriteSession\");\n }\n return session.writeScopes.some((pattern) =>\n scopeMatchesPattern(scope, pattern),\n );\n}\n\n/**\n * The media type the Personal Server stores for a binary write: the\n * `Content-Type` minus its parameters, `application/octet-stream` when blank.\n */\nexport function normalizeBinaryMimeType(contentType: string | null): string {\n if (!contentType) return \"application/octet-stream\";\n return contentType.split(\";\")[0].trim() || \"application/octet-stream\";\n}\n\n/**\n * How the Personal Server reads an `X-Vana-Metadata` header: JSON when it\n * parses, the raw string otherwise, `undefined` when absent or blank.\n */\nexport function parseWriteMetadataHeader(value: string | null): unknown {\n if (value === null) return undefined;\n const trimmed = value.trim();\n if (trimmed === \"\") return undefined;\n try {\n return JSON.parse(trimmed);\n } catch {\n return value;\n }\n}\n\n/**\n * Encode a metadata object for the `X-Vana-Metadata` header. Non-ASCII\n * characters are `\\uXXXX`-escaped so the value is header-safe everywhere;\n * it parses back to the same object.\n */\nexport function encodeWriteMetadataHeader(\n metadata: Record<string, unknown>,\n): string {\n let json: string;\n try {\n json = JSON.stringify(metadata);\n } catch (err) {\n throw new WriteRequestError(\n `metadata is not JSON-serialisable: ${errorMessage(err)}`,\n );\n }\n if (typeof json !== \"string\") {\n throw new WriteRequestError(\"metadata must serialise to JSON\");\n }\n return json.replace(\n /[\\u007f-\\uffff]/g,\n (c) => `\\\\u${c.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\nexport interface BinaryWriteSignedBytesInput {\n /** The raw body bytes the write sends. */\n bytes: Uint8Array;\n /** The `Content-Type` header the write sends (parameters are ignored). */\n contentType: string;\n /** The `X-Filename` header value the write sends, if any. */\n filename?: string;\n /** The exact `X-Vana-Metadata` header value the write sends, if any. */\n metadataHeader?: string;\n}\n\n/**\n * The bytes a builder signs for a binary write: the compact JSON of the\n * `$binary` record the Personal Server stores for these headers and bytes\n * (`personal-server-ts` `binaryWriteSignedBytes`, mirrored field for field).\n *\n * @returns UTF-8 bytes of the stored record's compact JSON.\n */\nexport function binaryWriteSignedBytes(\n input: BinaryWriteSignedBytesInput,\n): Uint8Array {\n const metadata = parseWriteMetadataHeader(input.metadataHeader ?? null);\n const record: Record<string, unknown> = {\n $binary: true,\n mimeType: normalizeBinaryMimeType(input.contentType),\n ...(input.filename ? { filename: input.filename } : {}),\n sizeBytes: input.bytes.length,\n contentHash: bytesToHex(sha256(input.bytes)),\n encoding: \"base64\",\n content: toBase64(input.bytes),\n ...(metadata !== undefined ? { metadata } : {}),\n };\n return new TextEncoder().encode(JSON.stringify(record));\n}\n\nconst PRINTABLE_ASCII = /^[\\x20-\\x7e]*$/;\n\n/**\n * Carry a filename the way the Personal Server reads it back verbatim:\n * `X-Filename` for printable ASCII, RFC 5987 `filename*` otherwise (a raw\n * non-ASCII header value is rejected by `fetch` or mangled in transit).\n */\nfunction setFilenameHeader(headers: Headers, filename: string): void {\n if (PRINTABLE_ASCII.test(filename)) {\n headers.set(WRITE_FILENAME_HEADER, filename);\n return;\n }\n headers.set(\n WRITE_CONTENT_DISPOSITION_HEADER,\n `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,\n );\n}\n\nfunction assertNoReservedKeys(\n value: Record<string, unknown>,\n where: string,\n): void {\n for (const key of RESERVED_WRITE_KEYS) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n throw new WriteRequestError(\n `${where} must not contain the reserved ${key} key; the Personal Server stamps it`,\n { key },\n );\n }\n }\n}\n\nfunction isLineagePair(\n value: unknown,\n): value is { ownerAddress: Address; scope: string } {\n return (\n isRecord(value) &&\n typeof value.ownerAddress === \"string\" &&\n typeof value.scope === \"string\"\n );\n}\n\n/**\n * Validate and lowercase the sources the way the Personal Server will\n * (distinct, at most {@link MAX_LINEAGE_SOURCES}, never the record's own id)\n * and apply the naming rule to every source given as a pair. `[]` is kept:\n * it is an explicit root statement.\n */\nfunction normalizeLineage(lineage: unknown, derivedScope: string): Hex[] {\n if (!Array.isArray(lineage)) {\n throw new WriteRequestError(\"lineage must be an array of data point ids\");\n }\n if (lineage.length > MAX_LINEAGE_SOURCES) {\n throw new WriteRequestError(\n `lineage lists ${lineage.length} sources; the maximum is ${MAX_LINEAGE_SOURCES}`,\n { max: MAX_LINEAGE_SOURCES, count: lineage.length },\n );\n }\n const seen = new Set<string>();\n const sources: Hex[] = [];\n const sourceScopes: string[] = [];\n const ownIds = new Set<string>();\n for (const entry of lineage as unknown[]) {\n let id: Hex;\n if (isLineagePair(entry)) {\n if (!isAddress(entry.ownerAddress, { strict: false })) {\n throw new WriteRequestError(\n \"lineage source ownerAddress must be an EVM address\",\n { ownerAddress: entry.ownerAddress },\n );\n }\n if (entry.scope.length === 0) {\n throw new WriteRequestError(\"lineage source scope is required\");\n }\n id = deriveDataPointId(entry.ownerAddress, entry.scope);\n sourceScopes.push(entry.scope);\n ownIds.add(deriveDataPointId(entry.ownerAddress, derivedScope));\n } else if (isDataPointId(entry)) {\n id = entry;\n } else {\n throw new WriteRequestError(\n \"lineage entries must be 32-byte hex data point ids or { ownerAddress, scope } pairs\",\n { entry },\n );\n }\n const normalized = id.toLowerCase() as Hex;\n if (seen.has(normalized)) {\n throw new WriteRequestError(\"lineage must not repeat a data point id\", {\n dataPointId: normalized,\n });\n }\n seen.add(normalized);\n sources.push(normalized);\n }\n for (const own of ownIds) {\n if (seen.has(own)) {\n throw new WriteRequestError(\n \"lineage must not cite the record's own data point\",\n { dataPointId: own, scope: derivedScope },\n );\n }\n }\n assertDerivedScopeNaming(derivedScope, sourceScopes);\n return sources;\n}\n\nfunction assertNoLineageField(\n value: Record<string, unknown>,\n where: string,\n): void {\n if (Object.prototype.hasOwnProperty.call(value, LINEAGE_FIELD)) {\n throw new WriteRequestError(\n `${where}.${LINEAGE_FIELD} is reserved; pass sources through the lineage option`,\n );\n }\n}\n\n/** The `X-Vana-Metadata` header for a binary write, or `undefined`. */\nfunction buildMetadataHeader(\n metadata: Record<string, unknown> | undefined,\n sources: Hex[] | undefined,\n): string | undefined {\n if (metadata !== undefined) {\n if (!isRecord(metadata)) {\n throw new WriteRequestError(\"metadata must be a plain object\");\n }\n assertNoReservedKeys(metadata, \"metadata\");\n assertNoLineageField(metadata, \"metadata\");\n }\n if (metadata === undefined && sources === undefined) return undefined;\n return encodeWriteMetadataHeader({\n ...(metadata ?? {}),\n ...(sources !== undefined ? { [LINEAGE_FIELD]: sources } : {}),\n });\n}\n\ninterface PreparedWrite {\n body: Uint8Array;\n /** What the proof's `bodyHash` commits to. */\n signedBytes: Uint8Array;\n contentType: string;\n filename?: string;\n metadataHeader?: string;\n}\n\nfunction prepareWrite(params: WriteDataParams): PreparedWrite {\n if (params.binary !== undefined && params.data !== undefined) {\n throw new WriteRequestError(\"Pass either data or binary, not both\");\n }\n // Absent or null lineage makes no statement: send nothing. Anything else\n // (`[]` included: an explicit root) is validated before it is touched,\n // since callers may be untyped.\n const rawLineage: unknown = params.lineage;\n const sources =\n rawLineage === undefined || rawLineage === null\n ? undefined\n : normalizeLineage(rawLineage, params.scope);\n\n if (params.binary !== undefined) {\n const { bytes, contentType, filename } = params.binary;\n if (!(bytes instanceof Uint8Array)) {\n throw new WriteRequestError(\"binary.bytes must be a Uint8Array\");\n }\n if (typeof contentType !== \"string\" || contentType.trim() === \"\") {\n throw new WriteRequestError(\"binary.contentType is required\");\n }\n if (filename !== undefined) {\n if (typeof filename !== \"string\") {\n throw new WriteRequestError(\"binary.filename must be a string\");\n }\n if (filename !== filename.trim()) {\n throw new WriteRequestError(\n \"binary.filename must not have leading or trailing whitespace\",\n );\n }\n }\n const metadataHeader = buildMetadataHeader(params.metadata, sources);\n return {\n body: bytes,\n signedBytes: binaryWriteSignedBytes({\n bytes,\n contentType,\n filename,\n metadataHeader,\n }),\n contentType,\n filename,\n metadataHeader,\n };\n }\n\n if (params.data === undefined) {\n throw new WriteRequestError(\"Pass data (a JSON object) or binary\");\n }\n if (!isRecord(params.data)) {\n throw new WriteRequestError(\"data must be a plain JSON object\");\n }\n if (params.metadata !== undefined) {\n throw new WriteRequestError(\n \"metadata applies to binary writes; put fields to store inside data\",\n );\n }\n assertNoReservedKeys(params.data, \"data\");\n assertNoLineageField(params.data, \"data\");\n const record =\n sources === undefined\n ? params.data\n : { ...params.data, [LINEAGE_FIELD]: sources };\n let text: string;\n try {\n // Compact JSON is the contract: the server re-serialises the parsed\n // record and requires it to match the signed bytes.\n text = JSON.stringify(record);\n } catch (err) {\n throw new WriteRequestError(\n `data is not JSON-serialisable: ${errorMessage(err)}`,\n );\n }\n if (typeof text !== \"string\" || !text.startsWith(\"{\")) {\n throw new WriteRequestError(\"data must serialise to a JSON object\");\n }\n const body = new TextEncoder().encode(text);\n return {\n body,\n signedBytes: body,\n contentType: \"application/json\",\n };\n}\n\nasync function writeErrorFromResponse(\n response: Response,\n): Promise<PersonalServerWriteError> {\n const { errorCode, message, details } =\n await readPersonalServerErrorBody(response);\n const text =\n message ??\n `Personal Server write failed: ${response.status} ${response.statusText}`;\n // Lineage rejections span 400 / 422 / 502; the code is the discriminator.\n if (response.status === 422 || errorCode?.startsWith(\"LINEAGE_\")) {\n return new WriteLineageError(text, response.status, errorCode, details);\n }\n switch (response.status) {\n case 401:\n return new WriteUnauthorizedError(text, errorCode, details);\n case 403:\n return new WriteForbiddenError(text, errorCode, details);\n case 409:\n return new WriteConflictError(text, errorCode, details);\n default:\n return new WriteRejectedError(text, response.status, errorCode, details);\n }\n}\n\n/**\n * Write one record into a scope under an open write session.\n *\n * @remarks\n * Sends `POST /v1/data/:scope` with `Authorization: Bearer <session token>`\n * and `X-Vana-Write-Signature`, a Web3Signed proof over the stored\n * representation carrying the session's `grantId` as a signed claim. JSON\n * writes send `data` as compact JSON with `Content-Type: application/json`;\n * binary writes send the bytes with their `Content-Type`, `X-Filename`, and\n * sign {@link binaryWriteSignedBytes}. `lineage` is the record's top-level\n * `lineage` field for JSON and the `lineage` field of `X-Vana-Metadata` for\n * binary, so the proof covers it either way.\n *\n * @returns The ingest answer (`scope`, `collectedAt`, `status`, and\n * `lineage.sources` when the write carried lineage).\n * @throws {WriteRequestError} Before sending: no payload, a reserved key, a\n * malformed lineage id, or non-object data.\n * @throws {WriteSessionExpiredError} Before sending: the session token has\n * passed its lifetime.\n * @throws {WriteUnauthorizedError} 401 (proof or session rejected).\n * @throws {WriteForbiddenError} 403 (grant no longer authorises the write).\n * @throws {WriteConflictError} 409.\n * @throws {WriteLineageError} Any `LINEAGE_*` rejection: 422\n * `LINEAGE_SOURCE_UNKNOWN` (`details.unknown`), 400 `LINEAGE_INVALID` /\n * `LINEAGE_SCOPE_UNDER_SOURCE_PREFIX`, 502 `LINEAGE_SOURCE_LOOKUP_FAILED`.\n * @throws {WriteRejectedError} Any other non-2xx.\n * @throws {WriteTransportError} `fetch` threw on every attempt.\n */\nexport async function writeData(\n params: WriteDataParams,\n): Promise<WriteDataResult> {\n const { session } = params;\n if (!isWriteSession(session)) {\n throw new WriteRequestError(\"session must come from openWriteSession\");\n }\n const fetchFn = resolveFetch(params.fetch);\n if (typeof params.scope !== \"string\" || params.scope.length === 0) {\n throw new WriteRequestError(\"scope is required\");\n }\n const prepared = prepareWrite(params);\n if (Date.now() >= session.expiresAt) {\n throw new WriteSessionExpiredError(\n \"Write session has expired; open a new session\",\n { grantId: session.grantId, expiresAt: session.expiresAt },\n );\n }\n\n const path = dataPath(params.scope);\n const response = await sendWithFreshProof(\n `Write to ${params.scope}`,\n fetchFn,\n params.retry,\n proofKeyFor({\n aud: session.audience,\n method: \"POST\",\n uri: path,\n grantId: session.grantId,\n signedBytes: prepared.signedBytes,\n }),\n async (iat) => {\n const headers = new Headers(params.headers);\n headers.set(\"Content-Type\", prepared.contentType);\n headers.set(\"Authorization\", `Bearer ${session.accessToken}`);\n if (prepared.filename) {\n setFilenameHeader(headers, prepared.filename);\n }\n if (prepared.metadataHeader !== undefined) {\n headers.set(WRITE_METADATA_HEADER, prepared.metadataHeader);\n }\n headers.set(\n WRITE_SIGNATURE_HEADER,\n await buildWeb3SignedHeader({\n signMessage: session.signer.signMessage,\n aud: session.audience,\n method: \"POST\",\n uri: path,\n body: prepared.signedBytes,\n grantId: session.grantId,\n iat,\n }),\n );\n return {\n url: `${session.personalServerUrl}${path}`,\n init: {\n method: \"POST\",\n headers,\n body: prepared.body as unknown as BodyInit,\n },\n };\n },\n );\n\n if (!response.ok) {\n throw await writeErrorFromResponse(response);\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch (err) {\n throw new WriteRejectedError(\n \"Personal Server write response is not JSON\",\n response.status,\n null,\n { cause: errorMessage(err) },\n );\n }\n const parsed = WriteDataResultSchema.safeParse(body);\n if (!parsed.success) {\n throw new WriteRejectedError(\n \"Personal Server write response is not an ingest result\",\n response.status,\n null,\n { issues: parsed.error.issues },\n );\n }\n return parsed.data as WriteDataResult;\n}\n\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown\n ? Omit<T, K>\n : never;\n\nexport type WritePersonalServerDataParams = Omit<\n OpenWriteSessionParams,\n \"fetch\" | \"headers\" | \"retry\"\n> &\n DistributiveOmit<WriteDataParams, \"session\">;\n\n/** {@link writePersonalServerData}'s answer: the ingest result plus the session it opened. */\nexport interface WritePersonalServerDataResult extends WriteDataResult {\n /** Reuse for further writes until `expiresAt`. */\n session: WriteSession;\n}\n\n/**\n * Open a write session and write one record in a single call.\n *\n * @remarks\n * Equivalent to {@link openWriteSession} followed by {@link writeData}. The\n * session is returned so further writes can reuse it; opening one per write\n * is correct but costs an extra signature and round-trip each time.\n *\n * @throws Everything {@link openWriteSession} and {@link writeData} throw.\n */\nexport async function writePersonalServerData(\n params: WritePersonalServerDataParams,\n): Promise<WritePersonalServerDataResult> {\n const session = await openWriteSession({\n personalServerUrl: params.personalServerUrl,\n signer: params.signer,\n grantId: params.grantId,\n account: params.account,\n audience: params.audience,\n fetch: params.fetch,\n headers: params.headers,\n retry: params.retry,\n });\n const writeParams = { ...params, session } as WriteDataParams;\n const result = await writeData(writeParams);\n return { ...result, session };\n}\n"],"mappings":"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","names":["bucket"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var scope_actions_exports = {};
|
|
20
|
+
__export(scope_actions_exports, {
|
|
21
|
+
InvalidScopeEntryError: () => InvalidScopeEntryError,
|
|
22
|
+
SCOPE_ACTIONS: () => SCOPE_ACTIONS,
|
|
23
|
+
formatScopeEntry: () => formatScopeEntry,
|
|
24
|
+
grantPermissions: () => grantPermissions,
|
|
25
|
+
hasAction: () => hasAction,
|
|
26
|
+
parseScopeEntry: () => parseScopeEntry,
|
|
27
|
+
permissionsToScopes: () => permissionsToScopes,
|
|
28
|
+
tryGrantPermissions: () => tryGrantPermissions
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(scope_actions_exports);
|
|
31
|
+
var import_scopes = require("./scopes");
|
|
32
|
+
const SCOPE_ACTIONS = ["read", "write"];
|
|
33
|
+
class InvalidScopeEntryError extends Error {
|
|
34
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
35
|
+
entry;
|
|
36
|
+
constructor(entry, reason) {
|
|
37
|
+
super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);
|
|
38
|
+
this.name = "InvalidScopeEntryError";
|
|
39
|
+
this.entry = entry;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const OPERATION_SEPARATOR = ":";
|
|
43
|
+
function describeValue(value) {
|
|
44
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
45
|
+
if (value === null) return "null";
|
|
46
|
+
return `[${typeof value}]`;
|
|
47
|
+
}
|
|
48
|
+
const OPERATION_BY_PREFIX = {
|
|
49
|
+
write: "write"
|
|
50
|
+
};
|
|
51
|
+
function assertScopePart(entry, scope) {
|
|
52
|
+
if (scope.length === 0) {
|
|
53
|
+
throw new InvalidScopeEntryError(entry, "scope part is empty");
|
|
54
|
+
}
|
|
55
|
+
if (scope.includes(OPERATION_SEPARATOR)) {
|
|
56
|
+
throw new InvalidScopeEntryError(
|
|
57
|
+
entry,
|
|
58
|
+
`scope part must not contain "${OPERATION_SEPARATOR}"`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseScopeEntry(entry) {
|
|
63
|
+
const raw = entry;
|
|
64
|
+
if (typeof raw !== "string") {
|
|
65
|
+
throw new InvalidScopeEntryError(raw, "entry must be a string");
|
|
66
|
+
}
|
|
67
|
+
const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);
|
|
68
|
+
if (separatorIndex === -1) {
|
|
69
|
+
assertScopePart(entry, entry);
|
|
70
|
+
return { scope: entry, action: "read" };
|
|
71
|
+
}
|
|
72
|
+
const prefix = entry.slice(0, separatorIndex);
|
|
73
|
+
const scope = entry.slice(separatorIndex + 1);
|
|
74
|
+
const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix) ? OPERATION_BY_PREFIX[prefix] : void 0;
|
|
75
|
+
if (action === void 0) {
|
|
76
|
+
throw new InvalidScopeEntryError(
|
|
77
|
+
entry,
|
|
78
|
+
`unknown operation "${prefix}" (known: ${Object.keys(OPERATION_BY_PREFIX).join(", ")}; read has no prefix)`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
assertScopePart(entry, scope);
|
|
82
|
+
return { scope, action };
|
|
83
|
+
}
|
|
84
|
+
function formatScopeEntry(parsed) {
|
|
85
|
+
const { scope, action } = parsed;
|
|
86
|
+
assertScopePart(scope, scope);
|
|
87
|
+
if (action === "read") return scope;
|
|
88
|
+
const prefix = Object.entries(OPERATION_BY_PREFIX).find(
|
|
89
|
+
([, candidate]) => candidate === action
|
|
90
|
+
)?.[0];
|
|
91
|
+
if (prefix === void 0) {
|
|
92
|
+
throw new InvalidScopeEntryError(
|
|
93
|
+
scope,
|
|
94
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return `${prefix}${OPERATION_SEPARATOR}${scope}`;
|
|
98
|
+
}
|
|
99
|
+
function compareScopes(a, b) {
|
|
100
|
+
if (a < b) return -1;
|
|
101
|
+
if (a > b) return 1;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
function sortActions(actions) {
|
|
105
|
+
const present = new Set(actions);
|
|
106
|
+
return SCOPE_ACTIONS.filter((action) => present.has(action));
|
|
107
|
+
}
|
|
108
|
+
function grantPermissions(scopes) {
|
|
109
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const entry of scopes) {
|
|
111
|
+
const { scope, action } = parseScopeEntry(entry);
|
|
112
|
+
let actions = byScope.get(scope);
|
|
113
|
+
if (actions === void 0) {
|
|
114
|
+
actions = /* @__PURE__ */ new Set();
|
|
115
|
+
byScope.set(scope, actions);
|
|
116
|
+
}
|
|
117
|
+
actions.add(action);
|
|
118
|
+
}
|
|
119
|
+
return [...byScope.keys()].sort(compareScopes).map((scope) => ({
|
|
120
|
+
scope,
|
|
121
|
+
actions: sortActions(byScope.get(scope) ?? [])
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
function permissionsToScopes(permissions) {
|
|
125
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const { scope, actions } of permissions) {
|
|
127
|
+
let merged = byScope.get(scope);
|
|
128
|
+
if (merged === void 0) {
|
|
129
|
+
merged = /* @__PURE__ */ new Set();
|
|
130
|
+
byScope.set(scope, merged);
|
|
131
|
+
}
|
|
132
|
+
for (const action of actions) {
|
|
133
|
+
if (!SCOPE_ACTIONS.includes(action)) {
|
|
134
|
+
throw new InvalidScopeEntryError(
|
|
135
|
+
scope,
|
|
136
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
merged.add(action);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const entries = [];
|
|
143
|
+
for (const scope of [...byScope.keys()].sort(compareScopes)) {
|
|
144
|
+
for (const action of sortActions(byScope.get(scope) ?? [])) {
|
|
145
|
+
entries.push(formatScopeEntry({ scope, action }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return entries;
|
|
149
|
+
}
|
|
150
|
+
function hasAction(scopes, scope, action) {
|
|
151
|
+
if (scope.includes(OPERATION_SEPARATOR)) return false;
|
|
152
|
+
for (const entry of scopes) {
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = parseScopeEntry(entry);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof InvalidScopeEntryError) continue;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if (parsed.action === action && (0, import_scopes.scopeMatchesPattern)(scope, parsed.scope)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function tryGrantPermissions(scopes) {
|
|
167
|
+
try {
|
|
168
|
+
return grantPermissions(scopes);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof InvalidScopeEntryError) return void 0;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
InvalidScopeEntryError,
|
|
177
|
+
SCOPE_ACTIONS,
|
|
178
|
+
formatScopeEntry,
|
|
179
|
+
grantPermissions,
|
|
180
|
+
hasAction,
|
|
181
|
+
parseScopeEntry,
|
|
182
|
+
permissionsToScopes,
|
|
183
|
+
tryGrantPermissions
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=scope-actions.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/scope-actions.ts"],"sourcesContent":["import { scopeMatchesPattern } from \"./scopes\";\n\n/**\n * Grant scope-entry grammar.\n *\n * A signed grant carries `scopes: string[]`. Each entry is\n * `[operation:]scope` - an optional lowercase ASCII operation prefix before\n * the first `:`, then a scope pattern (`*`, `{prefix}.*`, or an exact scope).\n * A missing prefix means read. `write:notes.entries` authorizes writing\n * `notes.entries` and nothing else; `notes.entries` authorizes reading it.\n *\n * The string form is the wire and storage detail: it is what the grantor\n * signs (EIP-712 `GrantRegistration.scopes`) and what the gateway stores\n * verbatim. The Personal Server is the sole interpreter, and this module is\n * the SDK-side mirror of that interpretation. Builders and consent UIs should\n * work with the grouped `{ scope, actions }` view (see\n * {@link grantPermissions}) and never construct or parse the strings by hand.\n *\n * Matching rules, pinned by the Personal Server policy\n * (personal-server-ts `packages/core/src/policy/data-write.ts` and\n * `data-read.ts`):\n * - the operation is compared exactly, case-sensitively;\n * - wildcards apply to the scope part only, via {@link scopeMatchesPattern};\n * - an entry whose operation is not recognised never authorizes anything.\n * The parser fails closed on it (throws) rather than treating it as read;\n * the matcher ({@link hasAction}) skips it, which is how the Personal\n * Server treats an entry it does not understand.\n */\n\n/** Operations the grammar defines today, in canonical (output) order. */\nexport const SCOPE_ACTIONS = [\"read\", \"write\"] as const;\n\n/** An operation a grant entry can authorize over a scope. */\nexport type ScopeAction = (typeof SCOPE_ACTIONS)[number];\n\n/** One grant entry, split into its operation and scope pattern. */\nexport interface ParsedScopeEntry {\n scope: string;\n action: ScopeAction;\n}\n\n/**\n * The grouped view of a grant's scope entries: one row per scope pattern\n * with every operation the grant authorizes over it.\n */\nexport interface GrantPermission {\n scope: string;\n actions: ScopeAction[];\n}\n\n/**\n * Thrown when a scope entry does not fit the grammar - an unknown or\n * malformed operation prefix, or an empty scope part.\n */\nexport class InvalidScopeEntryError extends Error {\n /** The offending entry, verbatim (unknown because it may not be a string). */\n readonly entry: unknown;\n\n constructor(entry: unknown, reason: string) {\n super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);\n this.name = \"InvalidScopeEntryError\";\n this.entry = entry;\n }\n}\n\nconst OPERATION_SEPARATOR = \":\";\n\n// Render an untrusted value for an error message without ever throwing:\n// String() and JSON.stringify() both defer to the value's own toString /\n// toJSON, which a hostile JSON body can make throw.\nfunction describeValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n if (value === null) return \"null\";\n return `[${typeof value}]`;\n}\n\n// The only operation that is ever written out. Read has no prefix, and\n// `read:` is NOT an alias for it: the Personal Server's read policy matches\n// entries verbatim, so a `read:x` entry would authorize nothing there, and\n// the parser must reject it for the same reason.\nconst OPERATION_BY_PREFIX: Readonly<Record<string, ScopeAction>> = {\n write: \"write\",\n};\n\nfunction assertScopePart(entry: string, scope: string): void {\n if (scope.length === 0) {\n throw new InvalidScopeEntryError(entry, \"scope part is empty\");\n }\n if (scope.includes(OPERATION_SEPARATOR)) {\n throw new InvalidScopeEntryError(\n entry,\n `scope part must not contain \"${OPERATION_SEPARATOR}\"`,\n );\n }\n}\n\n/**\n * Split one grant scope entry into its operation and scope pattern.\n *\n * - `notes.entries` parses as `{ scope: \"notes.entries\", action: \"read\" }`\n * - `write:notes.*` parses as `{ scope: \"notes.*\", action: \"write\" }`\n *\n * Fails closed: a non-string entry, or an entry whose operation prefix is\n * not recognised (including\n * `read:`, any uppercase or non-ASCII prefix, or a wildcard in the operation\n * position) throws {@link InvalidScopeEntryError} and is never treated as a\n * read entry. An empty scope part (`write:`) throws as well.\n *\n * @param entry - A single element of a grant's `scopes` array.\n * @returns The operation and the scope pattern it applies to.\n * @throws InvalidScopeEntryError when the entry does not fit the grammar.\n */\nexport function parseScopeEntry(entry: string): ParsedScopeEntry {\n // Grant bodies arrive from the network; a non-string element is a grammar\n // violation like any other, not a TypeError from indexOf.\n const raw: unknown = entry;\n if (typeof raw !== \"string\") {\n throw new InvalidScopeEntryError(raw, \"entry must be a string\");\n }\n const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);\n if (separatorIndex === -1) {\n assertScopePart(entry, entry);\n return { scope: entry, action: \"read\" };\n }\n\n const prefix = entry.slice(0, separatorIndex);\n const scope = entry.slice(separatorIndex + 1);\n const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix)\n ? OPERATION_BY_PREFIX[prefix]\n : undefined;\n if (action === undefined) {\n throw new InvalidScopeEntryError(\n entry,\n `unknown operation \"${prefix}\" (known: ${Object.keys(OPERATION_BY_PREFIX).join(\", \")}; read has no prefix)`,\n );\n }\n assertScopePart(entry, scope);\n return { scope, action };\n}\n\n/**\n * Inverse of {@link parseScopeEntry}: render one operation over one scope\n * pattern as a grant scope entry. Read has no prefix.\n *\n * @param parsed - The operation and scope pattern to encode.\n * @returns The wire-form entry, e.g. `write:notes.entries` or `notes.entries`.\n * @throws InvalidScopeEntryError when the action is unknown or the scope part\n * is empty or contains `:`.\n */\nexport function formatScopeEntry(parsed: ParsedScopeEntry): string {\n const { scope, action } = parsed;\n assertScopePart(scope, scope);\n if (action === \"read\") return scope;\n // Looked up rather than hard-coded so an action can never be emitted\n // without a prefix the parser accepts (and JS callers passing an unknown\n // action fail closed instead of producing a read entry).\n const prefix = Object.entries(OPERATION_BY_PREFIX).find(\n ([, candidate]) => candidate === action,\n )?.[0];\n if (prefix === undefined) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n return `${prefix}${OPERATION_SEPARATOR}${scope}`;\n}\n\nfunction compareScopes(a: string, b: string): number {\n // Plain code-unit order: locale-independent, so the grouping is identical\n // on every runtime.\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\nfunction sortActions(actions: Iterable<ScopeAction>): ScopeAction[] {\n const present = new Set(actions);\n return SCOPE_ACTIONS.filter((action) => present.has(action));\n}\n\n/**\n * Group a grant's scope entries into one `{ scope, actions }` row per scope\n * pattern - the view builders and consent UIs should render instead of the\n * raw strings.\n *\n * The result is canonical: rows are ordered by scope (code-unit order),\n * actions within a row follow {@link SCOPE_ACTIONS} order, and neither rows\n * nor actions repeat, whatever order or duplication the input had.\n *\n * Fails closed: if any entry does not fit the grammar this throws\n * {@link InvalidScopeEntryError} rather than silently dropping it, so a grant\n * carrying an operation this SDK does not know is never shown as narrower\n * than it is.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped, canonically ordered permissions.\n * @throws InvalidScopeEntryError when any entry does not fit the grammar.\n */\nexport function grantPermissions(scopes: readonly string[]): GrantPermission[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const entry of scopes) {\n const { scope, action } = parseScopeEntry(entry);\n let actions = byScope.get(scope);\n if (actions === undefined) {\n actions = new Set<ScopeAction>();\n byScope.set(scope, actions);\n }\n actions.add(action);\n }\n return [...byScope.keys()].sort(compareScopes).map((scope) => ({\n scope,\n actions: sortActions(byScope.get(scope) ?? []),\n }));\n}\n\n/**\n * Inverse of {@link grantPermissions}: flatten grouped permissions back into\n * the `string[]` form a grant is signed with.\n *\n * Output is canonical (scopes in code-unit order, read before write, no\n * duplicates), so `permissionsToScopes(grantPermissions(scopes))` is the\n * canonical form of `scopes`, and `grantPermissions(permissionsToScopes(p))`\n * is the canonical form of `p`. Rows with no actions contribute nothing; a\n * row with an action the grammar does not define throws rather than being\n * dropped.\n *\n * @param permissions - Grouped permissions, in any order, possibly repeating\n * a scope.\n * @returns The scope entries, one per (scope, action) pair.\n * @throws InvalidScopeEntryError when a scope or action does not fit the\n * grammar.\n */\nexport function permissionsToScopes(\n permissions: readonly GrantPermission[],\n): string[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const { scope, actions } of permissions) {\n let merged = byScope.get(scope);\n if (merged === undefined) {\n merged = new Set<ScopeAction>();\n byScope.set(scope, merged);\n }\n for (const action of actions) {\n if (!(SCOPE_ACTIONS as readonly string[]).includes(action)) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n merged.add(action);\n }\n }\n const entries: string[] = [];\n for (const scope of [...byScope.keys()].sort(compareScopes)) {\n for (const action of sortActions(byScope.get(scope) ?? [])) {\n entries.push(formatScopeEntry({ scope, action }));\n }\n }\n return entries;\n}\n\n/**\n * Does this grant authorize `action` over `scope`?\n *\n * The scope part is matched with the SDK's scope wildcard matcher\n * ({@link scopeMatchesPattern}: `*`, `{prefix}.*`, or exact), the action\n * exactly. Entries that do not fit the grammar are skipped - they authorize\n * nothing, which is exactly how the Personal Server treats them - so a grant\n * that carries an operation this SDK does not know still answers correctly\n * for the operations it does.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @param scope - The concrete scope being requested. Never prefixed: a value\n * containing `:` is not a scope id and yields `false`.\n * @param action - The operation being requested.\n * @returns `true` if some entry grants `action` over a pattern covering\n * `scope`.\n */\nexport function hasAction(\n scopes: readonly string[],\n scope: string,\n action: ScopeAction,\n): boolean {\n // A requested scope is a concrete scope id and never carries a prefix; the\n // Personal Server rejects anything else with ScopeSchema before it ever\n // reaches its matcher, so answer the same way here instead of letting\n // `write:x` fall through to a `*` entry.\n if (scope.includes(OPERATION_SEPARATOR)) return false;\n for (const entry of scopes) {\n let parsed: ParsedScopeEntry;\n try {\n parsed = parseScopeEntry(entry);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) continue;\n throw error;\n }\n if (parsed.action === action && scopeMatchesPattern(scope, parsed.scope)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * {@link grantPermissions} for a grant record read back from the gateway:\n * returns `undefined` instead of throwing when the scope list carries an\n * entry this SDK version cannot interpret, so a grant with a newer operation\n * still loads (with `scopes` intact) rather than failing the whole read.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped permissions, or `undefined` if any entry is\n * uninterpretable.\n */\nexport function tryGrantPermissions(\n scopes: readonly string[],\n): GrantPermission[] | undefined {\n try {\n return grantPermissions(scopes);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) return undefined;\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant scope-entry grammar.
|
|
3
|
+
*
|
|
4
|
+
* A signed grant carries `scopes: string[]`. Each entry is
|
|
5
|
+
* `[operation:]scope` - an optional lowercase ASCII operation prefix before
|
|
6
|
+
* the first `:`, then a scope pattern (`*`, `{prefix}.*`, or an exact scope).
|
|
7
|
+
* A missing prefix means read. `write:notes.entries` authorizes writing
|
|
8
|
+
* `notes.entries` and nothing else; `notes.entries` authorizes reading it.
|
|
9
|
+
*
|
|
10
|
+
* The string form is the wire and storage detail: it is what the grantor
|
|
11
|
+
* signs (EIP-712 `GrantRegistration.scopes`) and what the gateway stores
|
|
12
|
+
* verbatim. The Personal Server is the sole interpreter, and this module is
|
|
13
|
+
* the SDK-side mirror of that interpretation. Builders and consent UIs should
|
|
14
|
+
* work with the grouped `{ scope, actions }` view (see
|
|
15
|
+
* {@link grantPermissions}) and never construct or parse the strings by hand.
|
|
16
|
+
*
|
|
17
|
+
* Matching rules, pinned by the Personal Server policy
|
|
18
|
+
* (personal-server-ts `packages/core/src/policy/data-write.ts` and
|
|
19
|
+
* `data-read.ts`):
|
|
20
|
+
* - the operation is compared exactly, case-sensitively;
|
|
21
|
+
* - wildcards apply to the scope part only, via {@link scopeMatchesPattern};
|
|
22
|
+
* - an entry whose operation is not recognised never authorizes anything.
|
|
23
|
+
* The parser fails closed on it (throws) rather than treating it as read;
|
|
24
|
+
* the matcher ({@link hasAction}) skips it, which is how the Personal
|
|
25
|
+
* Server treats an entry it does not understand.
|
|
26
|
+
*/
|
|
27
|
+
/** Operations the grammar defines today, in canonical (output) order. */
|
|
28
|
+
export declare const SCOPE_ACTIONS: readonly ["read", "write"];
|
|
29
|
+
/** An operation a grant entry can authorize over a scope. */
|
|
30
|
+
export type ScopeAction = (typeof SCOPE_ACTIONS)[number];
|
|
31
|
+
/** One grant entry, split into its operation and scope pattern. */
|
|
32
|
+
export interface ParsedScopeEntry {
|
|
33
|
+
scope: string;
|
|
34
|
+
action: ScopeAction;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The grouped view of a grant's scope entries: one row per scope pattern
|
|
38
|
+
* with every operation the grant authorizes over it.
|
|
39
|
+
*/
|
|
40
|
+
export interface GrantPermission {
|
|
41
|
+
scope: string;
|
|
42
|
+
actions: ScopeAction[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when a scope entry does not fit the grammar - an unknown or
|
|
46
|
+
* malformed operation prefix, or an empty scope part.
|
|
47
|
+
*/
|
|
48
|
+
export declare class InvalidScopeEntryError extends Error {
|
|
49
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
50
|
+
readonly entry: unknown;
|
|
51
|
+
constructor(entry: unknown, reason: string);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Split one grant scope entry into its operation and scope pattern.
|
|
55
|
+
*
|
|
56
|
+
* - `notes.entries` parses as `{ scope: "notes.entries", action: "read" }`
|
|
57
|
+
* - `write:notes.*` parses as `{ scope: "notes.*", action: "write" }`
|
|
58
|
+
*
|
|
59
|
+
* Fails closed: a non-string entry, or an entry whose operation prefix is
|
|
60
|
+
* not recognised (including
|
|
61
|
+
* `read:`, any uppercase or non-ASCII prefix, or a wildcard in the operation
|
|
62
|
+
* position) throws {@link InvalidScopeEntryError} and is never treated as a
|
|
63
|
+
* read entry. An empty scope part (`write:`) throws as well.
|
|
64
|
+
*
|
|
65
|
+
* @param entry - A single element of a grant's `scopes` array.
|
|
66
|
+
* @returns The operation and the scope pattern it applies to.
|
|
67
|
+
* @throws InvalidScopeEntryError when the entry does not fit the grammar.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseScopeEntry(entry: string): ParsedScopeEntry;
|
|
70
|
+
/**
|
|
71
|
+
* Inverse of {@link parseScopeEntry}: render one operation over one scope
|
|
72
|
+
* pattern as a grant scope entry. Read has no prefix.
|
|
73
|
+
*
|
|
74
|
+
* @param parsed - The operation and scope pattern to encode.
|
|
75
|
+
* @returns The wire-form entry, e.g. `write:notes.entries` or `notes.entries`.
|
|
76
|
+
* @throws InvalidScopeEntryError when the action is unknown or the scope part
|
|
77
|
+
* is empty or contains `:`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function formatScopeEntry(parsed: ParsedScopeEntry): string;
|
|
80
|
+
/**
|
|
81
|
+
* Group a grant's scope entries into one `{ scope, actions }` row per scope
|
|
82
|
+
* pattern - the view builders and consent UIs should render instead of the
|
|
83
|
+
* raw strings.
|
|
84
|
+
*
|
|
85
|
+
* The result is canonical: rows are ordered by scope (code-unit order),
|
|
86
|
+
* actions within a row follow {@link SCOPE_ACTIONS} order, and neither rows
|
|
87
|
+
* nor actions repeat, whatever order or duplication the input had.
|
|
88
|
+
*
|
|
89
|
+
* Fails closed: if any entry does not fit the grammar this throws
|
|
90
|
+
* {@link InvalidScopeEntryError} rather than silently dropping it, so a grant
|
|
91
|
+
* carrying an operation this SDK does not know is never shown as narrower
|
|
92
|
+
* than it is.
|
|
93
|
+
*
|
|
94
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
95
|
+
* @returns The grouped, canonically ordered permissions.
|
|
96
|
+
* @throws InvalidScopeEntryError when any entry does not fit the grammar.
|
|
97
|
+
*/
|
|
98
|
+
export declare function grantPermissions(scopes: readonly string[]): GrantPermission[];
|
|
99
|
+
/**
|
|
100
|
+
* Inverse of {@link grantPermissions}: flatten grouped permissions back into
|
|
101
|
+
* the `string[]` form a grant is signed with.
|
|
102
|
+
*
|
|
103
|
+
* Output is canonical (scopes in code-unit order, read before write, no
|
|
104
|
+
* duplicates), so `permissionsToScopes(grantPermissions(scopes))` is the
|
|
105
|
+
* canonical form of `scopes`, and `grantPermissions(permissionsToScopes(p))`
|
|
106
|
+
* is the canonical form of `p`. Rows with no actions contribute nothing; a
|
|
107
|
+
* row with an action the grammar does not define throws rather than being
|
|
108
|
+
* dropped.
|
|
109
|
+
*
|
|
110
|
+
* @param permissions - Grouped permissions, in any order, possibly repeating
|
|
111
|
+
* a scope.
|
|
112
|
+
* @returns The scope entries, one per (scope, action) pair.
|
|
113
|
+
* @throws InvalidScopeEntryError when a scope or action does not fit the
|
|
114
|
+
* grammar.
|
|
115
|
+
*/
|
|
116
|
+
export declare function permissionsToScopes(permissions: readonly GrantPermission[]): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Does this grant authorize `action` over `scope`?
|
|
119
|
+
*
|
|
120
|
+
* The scope part is matched with the SDK's scope wildcard matcher
|
|
121
|
+
* ({@link scopeMatchesPattern}: `*`, `{prefix}.*`, or exact), the action
|
|
122
|
+
* exactly. Entries that do not fit the grammar are skipped - they authorize
|
|
123
|
+
* nothing, which is exactly how the Personal Server treats them - so a grant
|
|
124
|
+
* that carries an operation this SDK does not know still answers correctly
|
|
125
|
+
* for the operations it does.
|
|
126
|
+
*
|
|
127
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
128
|
+
* @param scope - The concrete scope being requested. Never prefixed: a value
|
|
129
|
+
* containing `:` is not a scope id and yields `false`.
|
|
130
|
+
* @param action - The operation being requested.
|
|
131
|
+
* @returns `true` if some entry grants `action` over a pattern covering
|
|
132
|
+
* `scope`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function hasAction(scopes: readonly string[], scope: string, action: ScopeAction): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* {@link grantPermissions} for a grant record read back from the gateway:
|
|
137
|
+
* returns `undefined` instead of throwing when the scope list carries an
|
|
138
|
+
* entry this SDK version cannot interpret, so a grant with a newer operation
|
|
139
|
+
* still loads (with `scopes` intact) rather than failing the whole read.
|
|
140
|
+
*
|
|
141
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
142
|
+
* @returns The grouped permissions, or `undefined` if any entry is
|
|
143
|
+
* uninterpretable.
|
|
144
|
+
*/
|
|
145
|
+
export declare function tryGrantPermissions(scopes: readonly string[]): GrantPermission[] | undefined;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { scopeMatchesPattern } from "./scopes.js";
|
|
2
|
+
const SCOPE_ACTIONS = ["read", "write"];
|
|
3
|
+
class InvalidScopeEntryError extends Error {
|
|
4
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
5
|
+
entry;
|
|
6
|
+
constructor(entry, reason) {
|
|
7
|
+
super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);
|
|
8
|
+
this.name = "InvalidScopeEntryError";
|
|
9
|
+
this.entry = entry;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const OPERATION_SEPARATOR = ":";
|
|
13
|
+
function describeValue(value) {
|
|
14
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
15
|
+
if (value === null) return "null";
|
|
16
|
+
return `[${typeof value}]`;
|
|
17
|
+
}
|
|
18
|
+
const OPERATION_BY_PREFIX = {
|
|
19
|
+
write: "write"
|
|
20
|
+
};
|
|
21
|
+
function assertScopePart(entry, scope) {
|
|
22
|
+
if (scope.length === 0) {
|
|
23
|
+
throw new InvalidScopeEntryError(entry, "scope part is empty");
|
|
24
|
+
}
|
|
25
|
+
if (scope.includes(OPERATION_SEPARATOR)) {
|
|
26
|
+
throw new InvalidScopeEntryError(
|
|
27
|
+
entry,
|
|
28
|
+
`scope part must not contain "${OPERATION_SEPARATOR}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseScopeEntry(entry) {
|
|
33
|
+
const raw = entry;
|
|
34
|
+
if (typeof raw !== "string") {
|
|
35
|
+
throw new InvalidScopeEntryError(raw, "entry must be a string");
|
|
36
|
+
}
|
|
37
|
+
const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);
|
|
38
|
+
if (separatorIndex === -1) {
|
|
39
|
+
assertScopePart(entry, entry);
|
|
40
|
+
return { scope: entry, action: "read" };
|
|
41
|
+
}
|
|
42
|
+
const prefix = entry.slice(0, separatorIndex);
|
|
43
|
+
const scope = entry.slice(separatorIndex + 1);
|
|
44
|
+
const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix) ? OPERATION_BY_PREFIX[prefix] : void 0;
|
|
45
|
+
if (action === void 0) {
|
|
46
|
+
throw new InvalidScopeEntryError(
|
|
47
|
+
entry,
|
|
48
|
+
`unknown operation "${prefix}" (known: ${Object.keys(OPERATION_BY_PREFIX).join(", ")}; read has no prefix)`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
assertScopePart(entry, scope);
|
|
52
|
+
return { scope, action };
|
|
53
|
+
}
|
|
54
|
+
function formatScopeEntry(parsed) {
|
|
55
|
+
const { scope, action } = parsed;
|
|
56
|
+
assertScopePart(scope, scope);
|
|
57
|
+
if (action === "read") return scope;
|
|
58
|
+
const prefix = Object.entries(OPERATION_BY_PREFIX).find(
|
|
59
|
+
([, candidate]) => candidate === action
|
|
60
|
+
)?.[0];
|
|
61
|
+
if (prefix === void 0) {
|
|
62
|
+
throw new InvalidScopeEntryError(
|
|
63
|
+
scope,
|
|
64
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return `${prefix}${OPERATION_SEPARATOR}${scope}`;
|
|
68
|
+
}
|
|
69
|
+
function compareScopes(a, b) {
|
|
70
|
+
if (a < b) return -1;
|
|
71
|
+
if (a > b) return 1;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
function sortActions(actions) {
|
|
75
|
+
const present = new Set(actions);
|
|
76
|
+
return SCOPE_ACTIONS.filter((action) => present.has(action));
|
|
77
|
+
}
|
|
78
|
+
function grantPermissions(scopes) {
|
|
79
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const entry of scopes) {
|
|
81
|
+
const { scope, action } = parseScopeEntry(entry);
|
|
82
|
+
let actions = byScope.get(scope);
|
|
83
|
+
if (actions === void 0) {
|
|
84
|
+
actions = /* @__PURE__ */ new Set();
|
|
85
|
+
byScope.set(scope, actions);
|
|
86
|
+
}
|
|
87
|
+
actions.add(action);
|
|
88
|
+
}
|
|
89
|
+
return [...byScope.keys()].sort(compareScopes).map((scope) => ({
|
|
90
|
+
scope,
|
|
91
|
+
actions: sortActions(byScope.get(scope) ?? [])
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function permissionsToScopes(permissions) {
|
|
95
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const { scope, actions } of permissions) {
|
|
97
|
+
let merged = byScope.get(scope);
|
|
98
|
+
if (merged === void 0) {
|
|
99
|
+
merged = /* @__PURE__ */ new Set();
|
|
100
|
+
byScope.set(scope, merged);
|
|
101
|
+
}
|
|
102
|
+
for (const action of actions) {
|
|
103
|
+
if (!SCOPE_ACTIONS.includes(action)) {
|
|
104
|
+
throw new InvalidScopeEntryError(
|
|
105
|
+
scope,
|
|
106
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
merged.add(action);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const scope of [...byScope.keys()].sort(compareScopes)) {
|
|
114
|
+
for (const action of sortActions(byScope.get(scope) ?? [])) {
|
|
115
|
+
entries.push(formatScopeEntry({ scope, action }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
function hasAction(scopes, scope, action) {
|
|
121
|
+
if (scope.includes(OPERATION_SEPARATOR)) return false;
|
|
122
|
+
for (const entry of scopes) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = parseScopeEntry(entry);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof InvalidScopeEntryError) continue;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.action === action && scopeMatchesPattern(scope, parsed.scope)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function tryGrantPermissions(scopes) {
|
|
137
|
+
try {
|
|
138
|
+
return grantPermissions(scopes);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof InvalidScopeEntryError) return void 0;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
InvalidScopeEntryError,
|
|
146
|
+
SCOPE_ACTIONS,
|
|
147
|
+
formatScopeEntry,
|
|
148
|
+
grantPermissions,
|
|
149
|
+
hasAction,
|
|
150
|
+
parseScopeEntry,
|
|
151
|
+
permissionsToScopes,
|
|
152
|
+
tryGrantPermissions
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=scope-actions.js.map
|