@fitzzero/quickdraw-core 5.1.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +533 -180
- package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
- package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/chunk-5CGO426C.js +15 -0
- package/dist/chunk-5CGO426C.js.map +1 -0
- package/dist/{chunk-A6DJBGOG.js → chunk-6K5BY47X.js} +4594 -3481
- package/dist/chunk-6K5BY47X.js.map +1 -0
- package/dist/{chunk-VJTFXHOK.js → chunk-753VJIUP.js} +13 -15
- package/dist/chunk-753VJIUP.js.map +1 -0
- package/dist/{chunk-JZBCLNDU.js → chunk-7QCUX4TK.js} +4 -4
- package/dist/chunk-7QCUX4TK.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/{chunk-3674EIRT.js → chunk-AONAFOYU.js} +14 -5
- package/dist/{chunk-3674EIRT.js.map → chunk-AONAFOYU.js.map} +1 -1
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
- package/dist/chunk-DUBU3BB3.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
- package/dist/chunk-GKH3LJ7N.js.map +1 -0
- package/dist/{chunk-KNDDCVQJ.js → chunk-GSIPUNJL.js} +49 -9
- package/dist/chunk-GSIPUNJL.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
- package/dist/chunk-LCC7HNFP.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
- package/dist/chunk-MBH6IDSM.js.map +1 -0
- package/dist/{chunk-2UELNHDD.js → chunk-NMXYENH4.js} +10 -10
- package/dist/{chunk-2UELNHDD.js.map → chunk-NMXYENH4.js.map} +1 -1
- package/dist/chunk-NO4M6PO5.js +101 -0
- package/dist/chunk-NO4M6PO5.js.map +1 -0
- package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
- package/dist/chunk-NW6U45HX.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
- package/dist/chunk-RJ5I2VO3.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
- package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
- package/dist/chunk-Z5ILULF2.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +5 -3
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +30 -18
- package/dist/client/index.js +8 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
- package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
- package/dist/index.d.ts +7 -7
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/prisma/index.d.ts +5 -5
- package/dist/prisma/index.js +130 -67
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +273 -268
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/express/index.d.ts +9 -2
- package/dist/server/express/index.js +1 -1
- package/dist/server/index.d.ts +147 -67
- package/dist/server/index.js +520 -381
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +63 -17
- package/dist/server/mcp/index.js +256 -54
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-DVa8W-yR.d.ts → sharing-UDLn1049.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +9 -7
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +12 -12
- package/dist/testing/index.js +21 -12
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +8 -6
- package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-A6DJBGOG.js.map +0 -1
- package/dist/chunk-CDKG4TFA.js.map +0 -1
- package/dist/chunk-DD4SKCNO.js.map +0 -1
- package/dist/chunk-H23KEFBA.js.map +0 -1
- package/dist/chunk-JZBCLNDU.js.map +0 -1
- package/dist/chunk-KNDDCVQJ.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VJTFXHOK.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → rateLimit-5RKBFLUR.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contract/kits/crud.ts","../src/contract/kits/sharingSchemas.ts","../src/contract/kits/sharing.ts"],"sourcesContent":["// The read/write kit's contract half (RFC 0003 section 12.1). `crud.contract`\n// returns ordinary `query` and `mutation` entries to spread into a contract's\n// `methods`, one per method it names and no others:\n//\n// export const task = defineContract(\"taskService\", {\n// entity: taskSchema,\n// projections: { card: cardSchema },\n// methods: {\n// ...crud.contract({\n// entity: taskSchema,\n// get: true,\n// list: { item: cardSchema, filter: [\"projectId\", \"status\"], sort: [\"ordinal\"] },\n// update: { input: taskPatch },\n// }),\n// rename: mutation({ ... }),\n// },\n// });\n//\n// The server half (`crud.handlers` on `./server`) finds the kit's methods in\n// the contract and implements exactly those: each entry carries what it\n// needs (`crudSpecOf`), and its type carries which kit method it is\n// (`CrudTag`), so the two halves cannot disagree. A client sees ordinary\n// members. About 270 of the 579 methods in the seven smaller 4.1 apps are\n// these shapes, each with its own paging and guards.\n\nimport type { RowSchema } from \"../defineContract\";\nimport {\n listOf,\n mutation,\n query,\n type MethodDef,\n type MutationDef,\n type ProjectionList,\n type QueryDef,\n} from \"../methods\";\nimport { isStandardSchema, type InferOutput, type StandardSchemaV1 } from \"../standardSchema\";\nimport { listInput, pageOutput, type ListPage, type ListQuery, type ListSort } from \"./crudList\";\nimport {\n bulkPatch,\n countOutput,\n CRUD_MAX_IDS,\n idInput,\n idsInput,\n nullOutput,\n reorderInput,\n withId,\n type BulkPatch,\n type BulkResult,\n type IdInput,\n type IdsInput,\n type ReorderInput,\n type WithId,\n} from \"./crudSchemas\";\nimport type { KitSchema } from \"./schemas\";\n\n/** The methods the read/write kit can add, each opt-in by name. */\nexport type CrudMethodName =\n | \"get\"\n | \"getMany\"\n | \"list\"\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"reorder\"\n | \"bulkUpdate\"\n | \"bulkDelete\";\n\n/** Every method of the read/write kit, in the order of RFC 0003 section 12.1. */\nexport const CRUD_METHODS: readonly CrudMethodName[] = Object.freeze([\n \"get\",\n \"getMany\",\n \"list\",\n \"create\",\n \"update\",\n \"delete\",\n \"reorder\",\n \"bulkUpdate\",\n \"bulkDelete\",\n]);\n\n/** Type-only: marks a method the read/write kit made, and which one. Never set. */\nexport interface CrudTag<Kind extends CrudMethodName> {\n readonly \"~crud\"?: Kind;\n}\n\ntype RowOf<Entity> = Entity extends StandardSchemaV1 ? InferOutput<Entity> : never;\n\n/** The entity's fields holding strings, numbers or booleans: what `list` filters and sorts on. */\nexport type ScalarFieldOf<Row> = {\n [Field in keyof Row]-?: NonNullable<Row[Field]> extends string | number | boolean ? Field : never;\n}[keyof Row] &\n string;\n\n/** The entity's fields holding numbers: a `reorder` column. */\nexport type NumberFieldOf<Row> = {\n [Field in keyof Row]-?: NonNullable<Row[Field]> extends number ? Field : never;\n}[keyof Row] &\n string;\n\ninterface Described {\n /** The method's description, for people and agents; the kit's own when absent. */\n readonly describe?: string;\n}\n\n/** Turns a method without options on: `true`, or `{ describe }`. */\nexport type CrudToggle = boolean | Described;\n\n/** `list`'s options. */\nexport interface CrudListOptions<Field extends string = string> extends Described {\n /**\n * The schema of each item: the contract's entity schema (the default) or\n * one of its projections' schemas, the same object.\n */\n readonly item?: RowSchema;\n /** The fields a caller may filter on, by equality. */\n readonly filter?: readonly Field[];\n /** The fields a caller may sort by; the first is the default order. */\n readonly sort?: readonly Field[];\n}\n\n/** `create`'s, `update`'s and `bulkUpdate`'s options: the app's schema of the fields written. */\nexport interface CrudInputOptions extends Described {\n /**\n * `create`: the new row's fields. `update` and `bulkUpdate`: the fields a\n * call may change, every one optional (`z.object({...}).partial()`); the\n * kit adds `id`, or `ids`.\n */\n readonly input: StandardSchemaV1;\n}\n\n/** `reorder`'s options. */\nexport interface CrudReorderOptions<\n Column extends string = string,\n Scope extends string = string,\n> extends Described {\n /** The numeric column rows are ordered by, ascending. */\n readonly column: Column;\n /** The columns whose values make one ordered list (`\"projectId\"`); the whole table when absent. */\n readonly within?: Scope | readonly Scope[];\n}\n\n/** `crud.contract`'s options: the entity, and each method to add. */\nexport interface CrudContractOptions {\n /** The contract's entity schema: the fields `list` and `reorder` name are its fields. */\n readonly entity: RowSchema;\n readonly get?: CrudToggle;\n readonly getMany?: CrudToggle;\n readonly list?: CrudListOptions;\n readonly create?: CrudInputOptions;\n readonly update?: CrudInputOptions;\n readonly delete?: CrudToggle;\n readonly reorder?: CrudReorderOptions;\n readonly bulkUpdate?: CrudInputOptions;\n readonly bulkDelete?: CrudToggle;\n}\n\ntype Quoted<Names> = `\"${Names & string}\"`;\n\ntype NamesIn<Value> = Value extends string\n ? Value\n : Value extends readonly (infer Name)[]\n ? Name\n : never;\n\ntype OptionNames<O, Option extends string, Key extends string> = O extends {\n readonly [K in Option]: { readonly [P in Key]: infer Value };\n}\n ? NamesIn<Value>\n : never;\n\n/** A broken option becomes a message: replacing the whole option keeps TypeScript from printing `never`. */\ntype Problem<Option extends string, Wrong, Text extends string> = [Wrong] extends [never]\n ? unknown\n : { readonly [K in Option]: `crud.contract: ${Option}: ${Quoted<Wrong>} ${Text}` };\n\n/** The checks against the entity's fields that the option types alone cannot state. */\ntype CrudChecks<O> = O extends { readonly entity: infer Entity }\n ? Problem<\n \"list\",\n Exclude<\n OptionNames<O, \"list\", \"filter\"> | OptionNames<O, \"list\", \"sort\">,\n ScalarFieldOf<RowOf<Entity>>\n >,\n \"is not a field of the entity holding strings, numbers or booleans\"\n > &\n Problem<\n \"reorder\",\n Exclude<OptionNames<O, \"reorder\", \"column\">, NumberFieldOf<RowOf<Entity>>>,\n \"is not a field of the entity holding numbers\"\n > &\n Problem<\n \"reorder\",\n Exclude<OptionNames<O, \"reorder\", \"within\">, ScalarFieldOf<RowOf<Entity>>>,\n \"is not a field of the entity holding strings, numbers or booleans\"\n >\n : unknown;\n\n/** What a `list` call passes: a filter and a sort over the declared fields, and paging. */\nexport type ListInput<Row, Filter extends string, Sort extends string> = {\n readonly filter?: [Filter] extends [never]\n ? undefined\n : { readonly [Field in Filter]?: Field extends keyof Row ? Row[Field] : never };\n readonly sort?: [Sort] extends [never] ? undefined : ListSort<Sort>;\n /** The `nextCursor` of the page before; the first page when absent. */\n readonly cursor?: string;\n /** The page size: default 50, at most 200 (a larger one is clamped). */\n readonly limit?: number;\n /** Also count every matching row (a second statement). */\n readonly totalCount?: boolean;\n};\n\nexport type CrudGet = QueryDef<KitSchema<IdInput>, \"entity\"> & CrudTag<\"get\">;\nexport type CrudGetMany = QueryDef<\n KitSchema<IdsInput, { readonly ids: string[] }>,\n ProjectionList<\"entity\">\n> &\n CrudTag<\"getMany\">;\nexport type CrudList<Row, Item, Filter extends string, Sort extends string> = QueryDef<\n KitSchema<ListInput<Row, Filter, Sort> | undefined, ListQuery<Filter, Sort>>,\n KitSchema<ListPage<Item>>\n> &\n CrudTag<\"list\">;\nexport type CrudCreate<Input extends StandardSchemaV1> = MutationDef<Input, \"entity\"> &\n CrudTag<\"create\">;\nexport type CrudUpdate<Patch extends StandardSchemaV1> = MutationDef<WithId<Patch>, \"entity\"> &\n CrudTag<\"update\">;\nexport type CrudDelete = MutationDef<KitSchema<IdInput>, KitSchema<null>> & CrudTag<\"delete\">;\nexport type CrudReorder = MutationDef<KitSchema<ReorderInput>, \"entity\"> & CrudTag<\"reorder\">;\nexport type CrudBulkUpdate<Patch extends StandardSchemaV1> = MutationDef<\n BulkPatch<Patch>,\n KitSchema<BulkResult>\n> &\n CrudTag<\"bulkUpdate\">;\nexport type CrudBulkDelete = MutationDef<\n KitSchema<IdsInput, { readonly ids: string[] }>,\n KitSchema<BulkResult>\n> &\n CrudTag<\"bulkDelete\">;\n\ntype EntityIn<O> = O extends { readonly entity: infer Entity } ? Entity : never;\n\ntype ListFieldsIn<O, Key extends \"filter\" | \"sort\"> = O extends {\n readonly list: { readonly [K in Key]: readonly (infer Field extends string)[] };\n}\n ? Field\n : never;\n\ntype ListItemIn<O> = O extends { readonly list: { readonly item: infer Item } }\n ? RowOf<Item>\n : RowOf<EntityIn<O>>;\n\ntype InputIn<O, Name extends \"create\" | \"update\" | \"bulkUpdate\"> = O extends {\n readonly [K in Name]: { readonly input: infer Input extends StandardSchemaV1 };\n}\n ? Input\n : never;\n\ntype CrudMethodOf<O, Name extends CrudMethodName> = {\n get: CrudGet;\n getMany: CrudGetMany;\n list: CrudList<\n RowOf<EntityIn<O>>,\n ListItemIn<O>,\n ListFieldsIn<O, \"filter\">,\n ListFieldsIn<O, \"sort\">\n >;\n create: CrudCreate<InputIn<O, \"create\">>;\n update: CrudUpdate<InputIn<O, \"update\">>;\n delete: CrudDelete;\n reorder: CrudReorder;\n bulkUpdate: CrudBulkUpdate<InputIn<O, \"bulkUpdate\">>;\n bulkDelete: CrudBulkDelete;\n}[Name];\n\ntype Enabled<O, Name extends CrudMethodName> = Name extends keyof O\n ? [O[Name]] extends [false | undefined]\n ? never\n : Name\n : never;\n\n/** The entries `crud.contract(options)` returns: one per method the options turn on. */\nexport type CrudMethods<O> = {\n readonly [Name in CrudMethodName as Enabled<O, Name>]: CrudMethodOf<O, Name>;\n};\n\n/** What the server half needs to know about one of the kit's methods. */\nexport type CrudSpec =\n | { readonly method: Exclude<CrudMethodName, \"list\" | \"reorder\"> }\n | {\n readonly method: \"list\";\n readonly filter: readonly string[];\n readonly sort: readonly string[];\n /** The item's schema; `undefined` for the entity. */\n readonly item: RowSchema | undefined;\n }\n | { readonly method: \"reorder\"; readonly column: string; readonly within: readonly string[] };\n\nconst SPECS = new WeakMap<object, CrudSpec>();\n\n/** What the read/write kit made `method` for, or `undefined` for any other method. */\nexport function crudSpecOf(method: unknown): CrudSpec | undefined {\n return typeof method === \"object\" && method !== null ? SPECS.get(method) : undefined;\n}\n\nconst DESCRIBE: Readonly<Record<CrudMethodName, string>> = Object.freeze({\n get: \"Reads one row by id.\",\n getMany: `Reads up to ${CRUD_MAX_IDS} rows by id, in the order asked; ids the caller cannot read, or with no row, are left out.`,\n list: \"Lists the rows the caller can read, a page at a time, filtered and sorted by the declared fields. Pass a page's nextCursor back as cursor for the next page.\",\n create: \"Creates a row.\",\n update: \"Changes the given fields of one row.\",\n delete: \"Deletes one row.\",\n reorder:\n \"Moves a row between two others: beforeId is the row that will come right before it, afterId the row right after; one is enough.\",\n bulkUpdate: `Changes the same fields on up to ${CRUD_MAX_IDS} rows, skipping rows the caller cannot change; returns how many changed.`,\n bulkDelete: `Deletes up to ${CRUD_MAX_IDS} rows, skipping rows the caller cannot delete; returns how many were deleted.`,\n});\n\ntype UnknownRecord = Readonly<Record<string, unknown>>;\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction fail(message: string): never {\n throw new TypeError(`crud.contract: ${message}`);\n}\n\nfunction checkKeys(owner: string, value: UnknownRecord, allowed: readonly string[]): void {\n const unknownKey = Object.keys(value).find((key) => !allowed.includes(key));\n if (unknownKey !== undefined) {\n fail(`${owner} has an unknown option \"${unknownKey}\"; the options are ${allowed.join(\", \")}`);\n }\n}\n\n/** The method's description: its own `describe`, or the kit's. */\nfunction describeOf(name: CrudMethodName, option: unknown): string {\n const own = isRecord(option) ? option.describe : undefined;\n if (own !== undefined && (typeof own !== \"string\" || own.length === 0)) {\n fail(`${name}: describe must be a non-empty string`);\n }\n return own ?? DESCRIBE[name];\n}\n\nfunction checkToggle(name: CrudMethodName, option: unknown): void {\n if (typeof option !== \"boolean\" && !isRecord(option)) {\n fail(`${name} must be true, false or { describe }`);\n }\n if (isRecord(option)) {\n checkKeys(name, option, [\"describe\"]);\n }\n}\n\nfunction checkFields(owner: string, fields: unknown): readonly string[] {\n if (fields === undefined) {\n return [];\n }\n const valid =\n Array.isArray(fields) &&\n fields.every((field) => typeof field === \"string\" && field.length > 0) &&\n new Set(fields).size === fields.length;\n if (!valid) {\n fail(`${owner} must be a list of distinct field names`);\n }\n return Object.freeze([...(fields as string[])]);\n}\n\nfunction listSpec(option: unknown): CrudSpec {\n if (!isRecord(option)) {\n fail(\"list must be { item?, filter?, sort?, describe? }\");\n }\n checkKeys(\"list\", option, [\"item\", \"filter\", \"sort\", \"describe\"]);\n if (option.item !== undefined && !isStandardSchema(option.item)) {\n fail(\"list.item must be the entity schema or one of the contract's projection schemas\");\n }\n return {\n method: \"list\",\n filter: checkFields(\"list.filter\", option.filter),\n sort: checkFields(\"list.sort\", option.sort),\n item: option.item as RowSchema | undefined,\n };\n}\n\nfunction inputOf(name: CrudMethodName, option: unknown): StandardSchemaV1 {\n if (!isRecord(option) || !isStandardSchema(option.input)) {\n fail(`${name} must be { input: <Standard Schema>, describe? }`);\n }\n checkKeys(name, option, [\"input\", \"describe\"]);\n return option.input;\n}\n\nfunction reorderSpec(option: unknown): CrudSpec {\n if (!isRecord(option) || typeof option.column !== \"string\" || option.column.length === 0) {\n fail(\"reorder must be { column, within?, describe? } with the name of a numeric column\");\n }\n checkKeys(\"reorder\", option, [\"column\", \"within\", \"describe\"]);\n const { column } = option;\n const within = checkFields(\n \"reorder.within\",\n typeof option.within === \"string\" ? [option.within] : option.within,\n );\n if (column === \"id\" || within.includes(column)) {\n fail(\"reorder.column must be a numeric column other than id and the within columns\");\n }\n return { method: \"reorder\", column, within };\n}\n\n/** One method of the kit: its entry, and what the server half needs to know about it. */\nfunction build(\n name: CrudMethodName,\n options: UnknownRecord,\n entity: StandardSchemaV1,\n): { def: MethodDef; spec: CrudSpec } {\n const option = options[name];\n const describe = describeOf(name, option);\n switch (name) {\n case \"list\": {\n const spec = listSpec(option) as Extract<CrudSpec, { method: \"list\" }>;\n const input = listInput(spec, entity);\n return { def: query({ input, output: pageOutput(spec.item ?? entity), describe }), spec };\n }\n case \"create\":\n return {\n def: mutation({ input: inputOf(name, option), output: \"entity\", describe }),\n spec: { method: name },\n };\n case \"update\":\n return {\n def: mutation({ input: withId(inputOf(name, option)), output: \"entity\", describe }),\n spec: { method: name },\n };\n case \"bulkUpdate\":\n return {\n def: mutation({ input: bulkPatch(inputOf(name, option)), output: countOutput(), describe }),\n spec: { method: name },\n };\n case \"reorder\":\n return {\n def: mutation({ input: reorderInput(), output: \"entity\", describe }),\n spec: reorderSpec(option),\n };\n default:\n checkToggle(name, option);\n return { def: toggled(name, describe), spec: { method: name } };\n }\n}\n\n/** The entry of a method without options. */\nfunction toggled(name: CrudMethodName, describe: string): MethodDef {\n switch (name) {\n case \"get\":\n return query({ input: idInput(), output: \"entity\", describe });\n case \"getMany\":\n return query({ input: idsInput(), output: listOf(\"entity\"), describe });\n case \"delete\":\n return mutation({ input: idInput(), output: nullOutput(), describe });\n default:\n return mutation({ input: idsInput(), output: countOutput(), describe });\n }\n}\n\nfunction checkOptions(options: unknown): UnknownRecord {\n if (!isRecord(options)) {\n fail(\"options must be { entity, ...methods }\");\n }\n checkKeys(\"the options\", options, [\"entity\", ...CRUD_METHODS]);\n if (!isStandardSchema(options.entity)) {\n fail(\"entity must be the contract's entity schema\");\n }\n return options;\n}\n\nfunction contract<const O extends CrudContractOptions>(\n options: O & NoInfer<CrudChecks<O>>,\n): CrudMethods<O> {\n const checked = checkOptions(options);\n const entity = checked.entity as StandardSchemaV1;\n const methods: Record<string, MethodDef> = {};\n for (const name of CRUD_METHODS) {\n const option = checked[name];\n if (option === undefined || option === false) {\n continue;\n }\n const { def, spec } = build(name, checked, entity);\n SPECS.set(def, Object.freeze(spec));\n methods[name] = def;\n }\n return Object.freeze(methods) as unknown as CrudMethods<O>;\n}\n\n/**\n * The read/write kit's contract half: `crud.contract(options)`. Each method is\n * opt-in by name (`get: true`, `list: { filter, sort }`, `update: { input }`),\n * and the server half (`crud.handlers` on `./server`) implements exactly the\n * methods named.\n */\nexport const crud = Object.freeze({ contract });\n","// The inputs and outputs the sharing and membership kit generates (RFC 0003\n// section 12.3). An access list method names its row as `id` and its user as\n// `userId`, and grants one of the levels a list entry can usefully hold\n// (`Read`, `Moderate` or `Admin`); a membership method names its row as\n// `entryId` and a stored role as `role`, which the server checks against the\n// service's `members` policy (the contract cannot know its roles). The\n// by-name methods take `name` or `email` instead of `userId`, for the\n// server's `resolveUser`. `listMembers` pages like `list` (`crudList.ts`):\n// 50 members by default, at most 200, by keyset cursor.\n//\n// Outputs: an access list as `ACL` (`[{ userId, level }]`, one entry per\n// user), a member as `{ userId, role, level }`, a page of members, or `null`.\n\nimport { ACCESS_LEVELS, isAccessLevel, type ACL, type AccessLevel } from \"../access\";\nimport type { StandardSchemaV1 } from \"../standardSchema\";\nimport { LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT, MAX_CURSOR_LENGTH, pagingIssues } from \"./crudList\";\nimport {\n idJson,\n isId,\n isRecord,\n kitSchema,\n objectJson,\n unknownKeys,\n type JsonSchema,\n type KitSchema,\n} from \"./schemas\";\n\n/** A level an access list entry is given: `Public` grants nothing, so it is not one. */\nexport type ShareLevel = Exclude<AccessLevel, \"Public\">;\n\n/** The levels `share` and `setLevel` grant, lowest first. */\nexport const SHARE_LEVELS: readonly ShareLevel[] = Object.freeze([\"Read\", \"Moderate\", \"Admin\"]);\n\n/** The longest role, name or email the kit accepts. */\nconst MAX_TEXT_LENGTH = 256;\n\n/** `share` and `setLevel`: give `userId` `level` on the row `id`. */\nexport interface ShareInput {\n readonly id: string;\n readonly userId: string;\n readonly level: ShareLevel;\n}\n\n/** `unshare`: take `userId`'s level on the row `id` away. */\nexport interface UnshareInput {\n readonly id: string;\n readonly userId: string;\n}\n\n/** Who a by-name method means: their name or their email, at least one. */\nexport interface UserLookupInput {\n readonly name?: string;\n readonly email?: string;\n}\n\n/** `shareByName`: `share`, with the user found by name or email. */\nexport interface ShareByNameInput extends UserLookupInput {\n readonly id: string;\n readonly level: ShareLevel;\n}\n\n/** `shareByName`'s input as its handler receives it. */\nexport interface ShareByNameQuery {\n readonly id: string;\n readonly name: string | undefined;\n readonly email: string | undefined;\n readonly level: ShareLevel;\n}\n\n/** `invite`: make `userId` a member of the row `entryId`, with `role` (the lowest by default). */\nexport interface InviteInput {\n readonly entryId: string;\n readonly userId: string;\n readonly role?: string;\n}\n\n/** `invite`'s input as its handler receives it. */\nexport interface InviteQuery {\n readonly entryId: string;\n readonly userId: string;\n readonly role: string | undefined;\n}\n\n/** `inviteByName`: `invite`, with the user found by name or email. */\nexport interface InviteByNameInput extends UserLookupInput {\n readonly entryId: string;\n readonly role?: string;\n}\n\n/** `inviteByName`'s input as its handler receives it. */\nexport interface InviteByNameQuery {\n readonly entryId: string;\n readonly name: string | undefined;\n readonly email: string | undefined;\n readonly role: string | undefined;\n}\n\n/** `remove`: end `userId`'s membership of the row `entryId`. */\nexport interface MemberInput {\n readonly entryId: string;\n readonly userId: string;\n}\n\n/** `leave`: end the caller's own membership of the row `entryId`. */\nexport interface LeaveInput {\n readonly entryId: string;\n}\n\n/** `setRole`: change `userId`'s role on the row `entryId`. */\nexport interface SetRoleInput {\n readonly entryId: string;\n readonly userId: string;\n readonly role: string;\n}\n\n/** `listMembers`: one page of the members of the row `entryId`. */\nexport interface ListMembersInput {\n readonly entryId: string;\n /** The `nextCursor` of the page before; the first page when absent. */\n readonly cursor?: string;\n /** The page size: default 50, at most 200 (a larger one is clamped). */\n readonly limit?: number;\n}\n\n/** `listMembers`' input as its handler receives it: the limit defaulted and clamped. */\nexport interface ListMembersQuery {\n readonly entryId: string;\n readonly cursor: string | undefined;\n readonly limit: number;\n}\n\n/** One member of a row. */\nexport interface Member {\n readonly userId: string;\n /** The role the membership table stores. */\n readonly role: string;\n /** The access level the role gives, or `null` for a role that gives none. */\n readonly level: AccessLevel | null;\n}\n\n/** One page of `listMembers`. */\nexport interface MembersPage {\n readonly items: Member[];\n /** Passed back as `cursor` for the next page; `null` on the last page. */\n readonly nextCursor: string | null;\n}\n\ntype Issues = StandardSchemaV1.Issue[];\n\n/** What one input key holds: a row or user id, a level, a role, or a name or email. */\ntype FieldKind = \"id\" | \"level\" | \"role\" | \"text\";\n\ninterface Field {\n readonly kind: FieldKind;\n readonly optional?: boolean;\n}\n\nconst MESSAGES: Readonly<Record<FieldKind, string>> = Object.freeze({\n id: \"Expected a non-empty string\",\n level: 'Expected \"Read\", \"Moderate\" or \"Admin\"',\n role: `Expected a role: a non-empty string of at most ${MAX_TEXT_LENGTH} characters`,\n text: `Expected a non-empty string of at most ${MAX_TEXT_LENGTH} characters`,\n});\n\nfunction isText(value: unknown): boolean {\n return typeof value === \"string\" && value.length > 0 && value.length <= MAX_TEXT_LENGTH;\n}\n\nfunction fits(kind: FieldKind, value: unknown): boolean {\n if (kind === \"id\") {\n return isId(value);\n }\n return kind === \"level\" ? SHARE_LEVELS.some((level) => level === value) : isText(value);\n}\n\nfunction fieldJson(kind: FieldKind): JsonSchema {\n if (kind === \"id\") {\n return idJson();\n }\n return kind === \"level\"\n ? { type: \"string\", enum: [...SHARE_LEVELS] }\n : { type: \"string\", minLength: 1, maxLength: MAX_TEXT_LENGTH };\n}\n\n/** How one input is checked and parsed. */\ninterface InputShape {\n readonly fields: Readonly<Record<string, Field>>;\n /** Keys checked by `issues` rather than by a field: `listMembers`' paging. */\n readonly more?: Readonly<Record<string, JsonSchema>>;\n /** Issues no single field shows: a by-name method needs a name or an email. */\n readonly issues?: (value: Readonly<Record<string, unknown>>) => Issues;\n /** The parsed input, when it is not the known keys as given. */\n readonly parse?: (value: Readonly<Record<string, unknown>>) => unknown;\n}\n\nfunction fieldIssues(shape: InputShape, value: Readonly<Record<string, unknown>>): Issues {\n return Object.entries(shape.fields).flatMap(([key, field]): Issues => {\n const given = value[key];\n const missing = given === undefined && field.optional !== true;\n const wrong = given !== undefined && !fits(field.kind, given);\n return missing || wrong ? [{ message: MESSAGES[field.kind], path: [key] }] : [];\n });\n}\n\nfunction inputJson(shape: InputShape): JsonSchema {\n const properties = Object.fromEntries(\n Object.entries(shape.fields).map(([key, field]) => [key, fieldJson(field.kind)]),\n );\n const required = Object.entries(shape.fields)\n .filter(([, field]) => field.optional !== true)\n .map(([key]) => key);\n return objectJson({ ...properties, ...shape.more }, required);\n}\n\n/** An input made of `shape`'s keys and nothing else. */\nfunction objectInput<Input, Parsed = Input>(shape: InputShape): KitSchema<Input, Parsed> {\n const keys = [...Object.keys(shape.fields), ...Object.keys(shape.more ?? {})];\n return kitSchema<Input, Parsed>(\n (input) => {\n if (!isRecord(input)) {\n return { issues: [{ message: \"Expected an object\", path: [] }] };\n }\n const issues = [\n ...unknownKeys(input, keys),\n ...fieldIssues(shape, input),\n ...(shape.issues?.(input) ?? []),\n ];\n if (issues.length > 0) {\n return { issues };\n }\n const parsed =\n shape.parse?.(input) ?? Object.fromEntries(keys.map((key) => [key, input[key]]));\n return { value: parsed as Parsed };\n },\n { input: () => inputJson(shape) },\n );\n}\n\nconst ID: Field = { kind: \"id\" };\nconst LEVEL: Field = { kind: \"level\" };\nconst ROLE: Field = { kind: \"role\" };\nconst OPTIONAL_ROLE: Field = { kind: \"role\", optional: true };\nconst OPTIONAL_TEXT: Field = { kind: \"text\", optional: true };\n\nfunction lookupIssues(value: Readonly<Record<string, unknown>>): Issues {\n return value.name === undefined && value.email === undefined\n ? [{ message: \"Give name or email\", path: [] }]\n : [];\n}\n\n/** `{ id, userId, level }`: `share`'s and `setLevel`'s input. */\nexport function shareInput(): KitSchema<ShareInput> {\n return objectInput<ShareInput>({ fields: { id: ID, userId: ID, level: LEVEL } });\n}\n\n/** `{ id, name?, email?, level }`: `shareByName`'s input. */\nexport function shareByNameInput(): KitSchema<ShareByNameInput, ShareByNameQuery> {\n return objectInput<ShareByNameInput, ShareByNameQuery>({\n fields: { id: ID, name: OPTIONAL_TEXT, email: OPTIONAL_TEXT, level: LEVEL },\n issues: lookupIssues,\n });\n}\n\n/** `{ id, userId }`: `unshare`'s input. */\nexport function unshareInput(): KitSchema<UnshareInput> {\n return objectInput<UnshareInput>({ fields: { id: ID, userId: ID } });\n}\n\n/** `{ entryId, userId, role? }`: `invite`'s input. */\nexport function inviteInput(): KitSchema<InviteInput, InviteQuery> {\n return objectInput<InviteInput, InviteQuery>({\n fields: { entryId: ID, userId: ID, role: OPTIONAL_ROLE },\n });\n}\n\n/** `{ entryId, name?, email?, role? }`: `inviteByName`'s input. */\nexport function inviteByNameInput(): KitSchema<InviteByNameInput, InviteByNameQuery> {\n return objectInput<InviteByNameInput, InviteByNameQuery>({\n fields: { entryId: ID, name: OPTIONAL_TEXT, email: OPTIONAL_TEXT, role: OPTIONAL_ROLE },\n issues: lookupIssues,\n });\n}\n\n/** `{ entryId, userId }`: `remove`'s input. */\nexport function memberInput(): KitSchema<MemberInput> {\n return objectInput<MemberInput>({ fields: { entryId: ID, userId: ID } });\n}\n\n/** `{ entryId }`: `leave`'s input. */\nexport function leaveInput(): KitSchema<LeaveInput> {\n return objectInput<LeaveInput>({ fields: { entryId: ID } });\n}\n\n/** `{ entryId, userId, role }`: `setRole`'s input. */\nexport function setRoleInput(): KitSchema<SetRoleInput> {\n return objectInput<SetRoleInput>({ fields: { entryId: ID, userId: ID, role: ROLE } });\n}\n\n/** `{ entryId, cursor?, limit? }`: `listMembers`' input. */\nexport function listMembersInput(): KitSchema<ListMembersInput, ListMembersQuery> {\n return objectInput<ListMembersInput, ListMembersQuery>({\n fields: { entryId: ID },\n more: {\n cursor: { type: \"string\", minLength: 1, maxLength: MAX_CURSOR_LENGTH },\n limit: { type: \"integer\", minimum: 1, maximum: LIST_MAX_LIMIT, default: LIST_DEFAULT_LIMIT },\n },\n issues: (value) => pagingIssues({ cursor: value.cursor, limit: value.limit }),\n parse: (value): ListMembersQuery => ({\n entryId: value.entryId as string,\n cursor: value.cursor as string | undefined,\n limit: Math.min((value.limit as number | undefined) ?? LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT),\n }),\n });\n}\n\nfunction shareIssues(entry: unknown, path: readonly PropertyKey[]): Issues {\n const valid =\n isRecord(entry) &&\n isId(entry.userId) &&\n isAccessLevel(entry.level) &&\n Object.keys(entry).length === 2;\n return valid ? [] : [{ message: \"Expected { userId, level }\", path: [...path] }];\n}\n\nfunction shareJson(): JsonSchema {\n return objectJson({ userId: idJson(), level: { type: \"string\", enum: [...ACCESS_LEVELS] } }, [\n \"userId\",\n \"level\",\n ]);\n}\n\n/** `[{ userId, level }]`: an access list, as `share`, `unshare`, `setLevel` and `listShares` return it. */\nexport function aclOutput(): KitSchema<ACL> {\n return kitSchema<ACL>(\n (value) => {\n if (!Array.isArray(value)) {\n return { issues: [{ message: \"Expected an access list\", path: [] }] };\n }\n const issues = value.flatMap((entry: unknown, index) => shareIssues(entry, [index]));\n return issues.length > 0 ? { issues } : { value: value as ACL };\n },\n { input: () => ({ type: \"array\", items: shareJson() }) },\n );\n}\n\nfunction memberIssues(member: unknown, path: readonly PropertyKey[]): Issues {\n const valid =\n isRecord(member) &&\n isId(member.userId) &&\n typeof member.role === \"string\" &&\n (member.level === null || isAccessLevel(member.level)) &&\n Object.keys(member).length === 3;\n return valid ? [] : [{ message: \"Expected { userId, role, level }\", path: [...path] }];\n}\n\nfunction memberJson(): JsonSchema {\n return objectJson(\n { userId: idJson(), role: { type: \"string\" }, level: { enum: [...ACCESS_LEVELS, null] } },\n [\"userId\", \"role\", \"level\"],\n );\n}\n\n/** `{ userId, role, level }`: a member, as `invite` and `setRole` return it. */\nexport function memberOutput(): KitSchema<Member> {\n return kitSchema<Member>(\n (value) => {\n const issues = memberIssues(value, []);\n return issues.length > 0 ? { issues } : { value: value as Member };\n },\n { input: memberJson },\n );\n}\n\n/** `{ items, nextCursor }`: one page of `listMembers`. */\nexport function membersPageOutput(): KitSchema<MembersPage> {\n return kitSchema<MembersPage>(\n (value) => {\n if (!isRecord(value) || !Array.isArray(value.items)) {\n return { issues: [{ message: \"Expected a page of members\", path: [] }] };\n }\n const issues = [\n ...unknownKeys(value, [\"items\", \"nextCursor\"]),\n ...value.items.flatMap((member: unknown, index) => memberIssues(member, [\"items\", index])),\n ];\n if (value.nextCursor !== null && typeof value.nextCursor !== \"string\") {\n issues.push({ message: \"Expected a cursor or null\", path: [\"nextCursor\"] });\n }\n return issues.length > 0 ? { issues } : { value: value as unknown as MembersPage };\n },\n {\n input: () =>\n objectJson(\n {\n items: { type: \"array\", items: memberJson() },\n nextCursor: { type: [\"string\", \"null\"] },\n },\n [\"items\", \"nextCursor\"],\n ),\n },\n );\n}\n","// The sharing and membership kit's contract half (RFC 0003 section 12.3).\n// `sharing.contract` returns ordinary `query` and `mutation` entries to\n// spread into a contract's `methods`, for one of two ways a service's rows\n// are shared:\n//\n// export const project = defineContract(\"projectService\", {\n// entity: projectSchema,\n// methods: {\n// ...sharing.contract({ mode: \"acl\" }), // share, unshare, setLevel, listShares\n// ...sharing.contract({ mode: \"members\", methods: [\"invite\", \"remove\", \"leave\", \"listMembers\"] }),\n// },\n// });\n//\n// `\"acl\"` edits the JSON access list a `jsonAcl` policy reads; `\"members\"`\n// edits the membership table a `members` policy reads. `methods` picks the\n// methods to add, by name; without it a mode adds all of its methods but the\n// by-name ones (`shareByName`, `inviteByName`), which find the user by name\n// or email through the server's `resolveUser`. The server half\n// (`sharing.handlers` on `./server`) finds the kit's entries in the contract\n// by what they were made for (`sharingSpecOf`), and its types by their tag\n// (`SharingTag`), so the two halves name the same methods.\n\nimport type { ACL } from \"../access\";\nimport { mutation, query, type MethodDef, type MutationDef, type QueryDef } from \"../methods\";\nimport { idInput, nullOutput, type IdInput } from \"./crudSchemas\";\nimport type { KitSchema } from \"./schemas\";\nimport {\n aclOutput,\n inviteByNameInput,\n inviteInput,\n leaveInput,\n listMembersInput,\n memberInput,\n memberOutput,\n membersPageOutput,\n setRoleInput,\n shareByNameInput,\n shareInput,\n unshareInput,\n type InviteByNameInput,\n type InviteByNameQuery,\n type InviteInput,\n type InviteQuery,\n type LeaveInput,\n type ListMembersInput,\n type ListMembersQuery,\n type Member,\n type MemberInput,\n type MembersPage,\n type SetRoleInput,\n type ShareByNameInput,\n type ShareByNameQuery,\n type ShareInput,\n type UnshareInput,\n} from \"./sharingSchemas\";\n\n/** How a service's rows are shared: a JSON access list (`jsonAcl`) or a membership table (`members`). */\nexport type SharingMode = \"acl\" | \"members\";\n\n/** The methods of `mode: \"acl\"`. */\nexport type AclMethodName = \"share\" | \"shareByName\" | \"unshare\" | \"setLevel\" | \"listShares\";\n\n/** The methods of `mode: \"members\"`. */\nexport type MembersMethodName =\n | \"invite\"\n | \"inviteByName\"\n | \"remove\"\n | \"leave\"\n | \"setRole\"\n | \"listMembers\";\n\n/** Every method the sharing kit can add. */\nexport type SharingMethodName = AclMethodName | MembersMethodName;\n\n/** The methods that find their user by name or email: opt-in, since they need `resolveUser`. */\nexport type SharingByNameMethod = \"shareByName\" | \"inviteByName\";\n\n/** Every method of each mode: those of RFC 0003 section 12.3, then the by-name one. */\nexport const SHARING_METHODS: {\n readonly acl: readonly AclMethodName[];\n readonly members: readonly MembersMethodName[];\n} = Object.freeze({\n acl: Object.freeze<AclMethodName[]>([\n \"share\",\n \"unshare\",\n \"setLevel\",\n \"listShares\",\n \"shareByName\",\n ]),\n members: Object.freeze<MembersMethodName[]>([\n \"invite\",\n \"remove\",\n \"leave\",\n \"setRole\",\n \"listMembers\",\n \"inviteByName\",\n ]),\n});\n\n/** Type-only: marks a method the sharing kit made, and which one. Never set. */\nexport interface SharingTag<Kind extends SharingMethodName> {\n readonly \"~sharing\"?: Kind;\n}\n\n/** `sharing.contract`'s options, for one mode. */\nexport type SharingContractOptions =\n | {\n readonly mode: \"acl\";\n /** The methods to add; all but `shareByName` when absent. */\n readonly methods?: readonly AclMethodName[];\n /** Descriptions for people and agents, per method; the kit's own for the rest. */\n readonly describe?: { readonly [Name in AclMethodName]?: string };\n }\n | {\n readonly mode: \"members\";\n /** The methods to add; all but `inviteByName` when absent. */\n readonly methods?: readonly MembersMethodName[];\n /** Descriptions for people and agents, per method; the kit's own for the rest. */\n readonly describe?: { readonly [Name in MembersMethodName]?: string };\n };\n\nexport type SharingShare = MutationDef<KitSchema<ShareInput>, KitSchema<ACL>> & SharingTag<\"share\">;\nexport type SharingShareByName = MutationDef<\n KitSchema<ShareByNameInput, ShareByNameQuery>,\n KitSchema<ACL>\n> &\n SharingTag<\"shareByName\">;\nexport type SharingUnshare = MutationDef<KitSchema<UnshareInput>, KitSchema<ACL>> &\n SharingTag<\"unshare\">;\nexport type SharingSetLevel = MutationDef<KitSchema<ShareInput>, KitSchema<ACL>> &\n SharingTag<\"setLevel\">;\nexport type SharingListShares = QueryDef<KitSchema<IdInput>, KitSchema<ACL>> &\n SharingTag<\"listShares\">;\nexport type SharingInvite = MutationDef<KitSchema<InviteInput, InviteQuery>, KitSchema<Member>> &\n SharingTag<\"invite\">;\nexport type SharingInviteByName = MutationDef<\n KitSchema<InviteByNameInput, InviteByNameQuery>,\n KitSchema<Member>\n> &\n SharingTag<\"inviteByName\">;\nexport type SharingRemove = MutationDef<KitSchema<MemberInput>, KitSchema<null>> &\n SharingTag<\"remove\">;\nexport type SharingLeave = MutationDef<KitSchema<LeaveInput>, KitSchema<null>> &\n SharingTag<\"leave\">;\nexport type SharingSetRole = MutationDef<KitSchema<SetRoleInput>, KitSchema<Member>> &\n SharingTag<\"setRole\">;\nexport type SharingListMembers = QueryDef<\n KitSchema<ListMembersInput, ListMembersQuery>,\n KitSchema<MembersPage>\n> &\n SharingTag<\"listMembers\">;\n\n/** The entry `sharing.contract` makes for method `Name`. */\nexport type SharingDefOf<Name extends SharingMethodName> = {\n share: SharingShare;\n shareByName: SharingShareByName;\n unshare: SharingUnshare;\n setLevel: SharingSetLevel;\n listShares: SharingListShares;\n invite: SharingInvite;\n inviteByName: SharingInviteByName;\n remove: SharingRemove;\n leave: SharingLeave;\n setRole: SharingSetRole;\n listMembers: SharingListMembers;\n}[Name];\n\ntype ModeIn<O> = O extends { readonly mode: infer Mode } ? Mode : never;\n\ntype DefaultNames<Mode> = Mode extends \"acl\"\n ? Exclude<AclMethodName, SharingByNameMethod>\n : Mode extends \"members\"\n ? Exclude<MembersMethodName, SharingByNameMethod>\n : never;\n\ntype NamesIn<O> = O extends { readonly methods: readonly (infer Name extends SharingMethodName)[] }\n ? Name\n : DefaultNames<ModeIn<O>>;\n\n/** The entries `sharing.contract(options)` returns: one per method it adds. */\nexport type SharingMethods<O> = { readonly [Name in NamesIn<O>]: SharingDefOf<Name> };\n\n/** What the server half needs to know about a method the sharing kit made. */\nexport interface SharingSpec {\n readonly method: SharingMethodName;\n readonly mode: SharingMode;\n}\n\nconst SPECS = new WeakMap<object, SharingSpec>();\n\n/** What the sharing kit made `method` for, or `undefined` for any other method. */\nexport function sharingSpecOf(method: unknown): SharingSpec | undefined {\n return typeof method === \"object\" && method !== null ? SPECS.get(method) : undefined;\n}\n\nconst DESCRIBE: Readonly<Record<SharingMethodName, string>> = Object.freeze({\n share:\n \"Gives a user a level on one row: Read, Moderate or Admin. A user the row is shared with already gets the new level.\",\n shareByName:\n \"Gives the user with this name or email a level on one row: Read, Moderate or Admin.\",\n unshare: \"Takes a user's level on one row away. The owner's cannot be taken away.\",\n setLevel: \"Changes the level of a user one row is already shared with.\",\n listShares: \"Lists the users one row is shared with, and their levels.\",\n invite: \"Makes a user a member of one row, with a role (the lowest role when none is given).\",\n inviteByName:\n \"Makes the user with this name or email a member of one row, with a role (the lowest role when none is given).\",\n remove: \"Ends a user's membership of one row. The last Admin member cannot be removed.\",\n leave: \"Ends the caller's own membership of one row. The last Admin member cannot leave.\",\n setRole: \"Changes a member's role on one row.\",\n listMembers:\n \"Lists the members of one row and their roles, a page at a time. Pass a page's nextCursor back as cursor for the next page.\",\n});\n\nconst BUILD: Readonly<Record<SharingMethodName, (describe: string) => MethodDef>> = Object.freeze({\n share: (describe) => mutation({ input: shareInput(), output: aclOutput(), describe }),\n shareByName: (describe) => mutation({ input: shareByNameInput(), output: aclOutput(), describe }),\n unshare: (describe) => mutation({ input: unshareInput(), output: aclOutput(), describe }),\n setLevel: (describe) => mutation({ input: shareInput(), output: aclOutput(), describe }),\n listShares: (describe) => query({ input: idInput(), output: aclOutput(), describe }),\n invite: (describe) => mutation({ input: inviteInput(), output: memberOutput(), describe }),\n inviteByName: (describe) =>\n mutation({ input: inviteByNameInput(), output: memberOutput(), describe }),\n remove: (describe) => mutation({ input: memberInput(), output: nullOutput(), describe }),\n leave: (describe) => mutation({ input: leaveInput(), output: nullOutput(), describe }),\n setRole: (describe) => mutation({ input: setRoleInput(), output: memberOutput(), describe }),\n listMembers: (describe) =>\n query({ input: listMembersInput(), output: membersPageOutput(), describe }),\n});\n\ntype UnknownRecord = Readonly<Record<string, unknown>>;\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction fail(message: string): never {\n throw new TypeError(`sharing.contract: ${message}`);\n}\n\nfunction quoted(names: readonly string[]): string {\n return names.map((name) => `\"${name}\"`).join(\", \");\n}\n\nfunction modeOf(options: unknown): SharingMode {\n if (!isRecord(options)) {\n fail('options must be { mode: \"acl\" | \"members\", methods?, describe? }');\n }\n const unknownKey = Object.keys(options).find(\n (key) => ![\"mode\", \"methods\", \"describe\"].includes(key),\n );\n if (unknownKey !== undefined) {\n fail(`unknown option \"${unknownKey}\"; the options are mode, methods and describe`);\n }\n const { mode } = options;\n if (mode !== \"acl\" && mode !== \"members\") {\n fail('mode must be \"acl\" (a jsonAcl access list) or \"members\" (a members table)');\n }\n return mode;\n}\n\n/** The methods the options add: those `methods` names, or the mode's default ones. */\nfunction namesOf(mode: SharingMode, methods: unknown): readonly SharingMethodName[] {\n const all: readonly SharingMethodName[] = SHARING_METHODS[mode];\n if (methods === undefined) {\n return all.filter((name) => name !== \"shareByName\" && name !== \"inviteByName\");\n }\n const valid =\n Array.isArray(methods) &&\n methods.length > 0 &&\n methods.every((name) => all.some((known) => known === name)) &&\n new Set(methods).size === methods.length;\n if (!valid) {\n fail(`methods must name one or more distinct methods of mode \"${mode}\": ${quoted(all)}`);\n }\n return methods as SharingMethodName[];\n}\n\n/** The descriptions the options give, each for a method they add. */\nfunction describeOf(names: readonly SharingMethodName[], describe: unknown): UnknownRecord {\n if (describe === undefined) {\n return {};\n }\n if (!isRecord(describe)) {\n fail(\"describe must map method names to descriptions\");\n }\n for (const [name, text] of Object.entries(describe)) {\n if (!names.some((added) => added === name)) {\n fail(`describe names \"${name}\", which these options do not add`);\n }\n if (typeof text !== \"string\" || text.length === 0) {\n fail(`describe for \"${name}\" must be a non-empty string`);\n }\n }\n return describe;\n}\n\nfunction contract<const O extends SharingContractOptions>(options: O): SharingMethods<O> {\n const mode = modeOf(options);\n const names = namesOf(mode, options.methods);\n const described = describeOf(names, options.describe);\n const methods: Record<string, MethodDef> = {};\n for (const name of names) {\n const own = described[name];\n const def = BUILD[name](typeof own === \"string\" ? own : DESCRIBE[name]);\n SPECS.set(def, Object.freeze({ method: name, mode }));\n methods[name] = def;\n }\n return Object.freeze(methods) as unknown as SharingMethods<O>;\n}\n\n/**\n * The sharing and membership kit's contract half: `sharing.contract({ mode,\n * methods?, describe? })` makes the methods that share a row through its\n * JSON access list (`mode: \"acl\"`) or its membership table\n * (`mode: \"members\"`), which the server half (`sharing.handlers` on\n * `./server`) implements.\n */\nexport const sharing = Object.freeze({ contract });\n"],"mappings":"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","names":["isRecord","SPECS","DESCRIBE","isRecord","fail","describeOf","contract"]}
|
|
1
|
+
{"version":3,"sources":["../src/contract/kits/crud.ts","../src/contract/kits/sharingSchemas.ts","../src/contract/kits/sharing.ts"],"sourcesContent":["// The read/write kit's contract half (RFC 0003 section 12.1). `crud.contract`\n// returns ordinary `query` and `mutation` entries to spread into a contract's\n// `methods`, one per method it names and no others:\n//\n// export const task = defineContract(\"taskService\", {\n// entity: taskSchema,\n// projections: { card: cardSchema },\n// methods: {\n// ...crud.contract({\n// entity: taskSchema,\n// get: true,\n// list: { item: cardSchema, filter: [\"projectId\", \"status\"], sort: [\"ordinal\"] },\n// update: { input: taskPatch },\n// }),\n// rename: mutation({ ... }),\n// },\n// });\n//\n// The server half (`crud.handlers` on `./server`) finds the kit's methods in\n// the contract and implements exactly those: each entry carries what it\n// needs (`crudSpecOf`), and its type carries which kit method it is\n// (`CrudTag`), so the two halves cannot disagree. A client sees ordinary\n// members. About 270 of the 579 methods in the seven smaller 4.1 apps are\n// these shapes, each with its own paging and guards.\n\nimport type { RowSchema } from \"../defineContract\";\nimport {\n listOf,\n mutation,\n query,\n type MethodDef,\n type MutationDef,\n type ProjectionList,\n type QueryDef,\n} from \"../methods\";\nimport { isStandardSchema, type InferOutput, type StandardSchemaV1 } from \"../standardSchema\";\nimport { listInput, pageOutput, type ListPage, type ListQuery, type ListSort } from \"./crudList\";\nimport {\n bulkPatch,\n countOutput,\n CRUD_MAX_IDS,\n idInput,\n idsInput,\n nullOutput,\n reorderInput,\n withId,\n type BulkPatch,\n type BulkResult,\n type IdInput,\n type IdsInput,\n type ReorderInput,\n type WithId,\n} from \"./crudSchemas\";\nimport type { KitSchema } from \"./schemas\";\n\n/** The methods the read/write kit can add, each opt-in by name. */\nexport type CrudMethodName =\n | \"get\"\n | \"getMany\"\n | \"list\"\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"reorder\"\n | \"bulkUpdate\"\n | \"bulkDelete\";\n\n/** Every method of the read/write kit, in the order of RFC 0003 section 12.1. */\nexport const CRUD_METHODS: readonly CrudMethodName[] = Object.freeze([\n \"get\",\n \"getMany\",\n \"list\",\n \"create\",\n \"update\",\n \"delete\",\n \"reorder\",\n \"bulkUpdate\",\n \"bulkDelete\",\n]);\n\n/** Type-only: marks a method the read/write kit made, and which one. Never set. */\nexport interface CrudTag<Kind extends CrudMethodName> {\n readonly \"~crud\"?: Kind;\n}\n\ntype RowOf<Entity> = Entity extends StandardSchemaV1 ? InferOutput<Entity> : never;\n\n/** The entity's fields holding strings, numbers or booleans: what `list` filters and sorts on. */\nexport type ScalarFieldOf<Row> = {\n [Field in keyof Row]-?: NonNullable<Row[Field]> extends string | number | boolean ? Field : never;\n}[keyof Row] &\n string;\n\n/** The entity's fields holding numbers: a `reorder` column. */\nexport type NumberFieldOf<Row> = {\n [Field in keyof Row]-?: NonNullable<Row[Field]> extends number ? Field : never;\n}[keyof Row] &\n string;\n\ninterface Described {\n /** The method's description, for people and agents; the kit's own when absent. */\n readonly describe?: string;\n}\n\n/** Turns a method without options on: `true`, or `{ describe }`. */\nexport type CrudToggle = boolean | Described;\n\n/** `list`'s options. */\nexport interface CrudListOptions<Field extends string = string> extends Described {\n /**\n * The schema of each item: the contract's entity schema (the default) or\n * one of its projections' schemas, the same object.\n */\n readonly item?: RowSchema;\n /** The fields a caller may filter on, by equality. */\n readonly filter?: readonly Field[];\n /** The fields a caller may sort by; the first is the default order. */\n readonly sort?: readonly Field[];\n}\n\n/** `create`'s, `update`'s and `bulkUpdate`'s options: the app's schema of the fields written. */\nexport interface CrudInputOptions extends Described {\n /**\n * `create`: the new row's fields. `update` and `bulkUpdate`: the fields a\n * call may change, every one optional (`z.object({...}).partial()`); the\n * kit adds `id`, or `ids`.\n */\n readonly input: StandardSchemaV1;\n}\n\n/** `reorder`'s options. */\nexport interface CrudReorderOptions<\n Column extends string = string,\n Scope extends string = string,\n> extends Described {\n /** The numeric column rows are ordered by, ascending. */\n readonly column: Column;\n /** The columns whose values make one ordered list (`\"projectId\"`); the whole table when absent. */\n readonly within?: Scope | readonly Scope[];\n}\n\n/** `crud.contract`'s options: the entity, and each method to add. */\nexport interface CrudContractOptions {\n /** The contract's entity schema: the fields `list` and `reorder` name are its fields. */\n readonly entity: RowSchema;\n readonly get?: CrudToggle;\n readonly getMany?: CrudToggle;\n readonly list?: CrudListOptions;\n readonly create?: CrudInputOptions;\n readonly update?: CrudInputOptions;\n readonly delete?: CrudToggle;\n readonly reorder?: CrudReorderOptions;\n readonly bulkUpdate?: CrudInputOptions;\n readonly bulkDelete?: CrudToggle;\n}\n\ntype Quoted<Names> = `\"${Names & string}\"`;\n\ntype NamesIn<Value> = Value extends string\n ? Value\n : Value extends readonly (infer Name)[]\n ? Name\n : never;\n\ntype OptionNames<O, Option extends string, Key extends string> = O extends {\n readonly [K in Option]: { readonly [P in Key]: infer Value };\n}\n ? NamesIn<Value>\n : never;\n\n/** A broken option becomes a message: replacing the whole option keeps TypeScript from printing `never`. */\ntype Problem<Option extends string, Wrong, Text extends string> = [Wrong] extends [never]\n ? unknown\n : { readonly [K in Option]: `crud.contract: ${Option}: ${Quoted<Wrong>} ${Text}` };\n\n/** The checks against the entity's fields that the option types alone cannot state. */\ntype CrudChecks<O> = O extends { readonly entity: infer Entity }\n ? Problem<\n \"list\",\n Exclude<\n OptionNames<O, \"list\", \"filter\"> | OptionNames<O, \"list\", \"sort\">,\n ScalarFieldOf<RowOf<Entity>>\n >,\n \"is not a field of the entity holding strings, numbers or booleans\"\n > &\n Problem<\n \"reorder\",\n Exclude<OptionNames<O, \"reorder\", \"column\">, NumberFieldOf<RowOf<Entity>>>,\n \"is not a field of the entity holding numbers\"\n > &\n Problem<\n \"reorder\",\n Exclude<OptionNames<O, \"reorder\", \"within\">, ScalarFieldOf<RowOf<Entity>>>,\n \"is not a field of the entity holding strings, numbers or booleans\"\n >\n : unknown;\n\n/** What a `list` call passes: a filter and a sort over the declared fields, and paging. */\nexport type ListInput<Row, Filter extends string, Sort extends string> = {\n readonly filter?: [Filter] extends [never]\n ? undefined\n : { readonly [Field in Filter]?: Field extends keyof Row ? Row[Field] : never };\n readonly sort?: [Sort] extends [never] ? undefined : ListSort<Sort>;\n /** The `nextCursor` of the page before; the first page when absent. */\n readonly cursor?: string;\n /** The page size: default 50, at most 200 (a larger one is clamped). */\n readonly limit?: number;\n /** Also count every matching row (a second statement). */\n readonly totalCount?: boolean;\n};\n\nexport type CrudGet = QueryDef<KitSchema<IdInput>, \"entity\"> & CrudTag<\"get\">;\nexport type CrudGetMany = QueryDef<\n KitSchema<IdsInput, { readonly ids: string[] }>,\n ProjectionList<\"entity\">\n> &\n CrudTag<\"getMany\">;\nexport type CrudList<Row, Item, Filter extends string, Sort extends string> = QueryDef<\n KitSchema<ListInput<Row, Filter, Sort> | undefined, ListQuery<Filter, Sort>>,\n KitSchema<ListPage<Item>>\n> &\n CrudTag<\"list\">;\nexport type CrudCreate<Input extends StandardSchemaV1> = MutationDef<Input, \"entity\"> &\n CrudTag<\"create\">;\nexport type CrudUpdate<Patch extends StandardSchemaV1> = MutationDef<WithId<Patch>, \"entity\"> &\n CrudTag<\"update\">;\nexport type CrudDelete = MutationDef<KitSchema<IdInput>, KitSchema<null>> & CrudTag<\"delete\">;\nexport type CrudReorder = MutationDef<KitSchema<ReorderInput>, \"entity\"> & CrudTag<\"reorder\">;\nexport type CrudBulkUpdate<Patch extends StandardSchemaV1> = MutationDef<\n BulkPatch<Patch>,\n KitSchema<BulkResult>\n> &\n CrudTag<\"bulkUpdate\">;\nexport type CrudBulkDelete = MutationDef<\n KitSchema<IdsInput, { readonly ids: string[] }>,\n KitSchema<BulkResult>\n> &\n CrudTag<\"bulkDelete\">;\n\ntype EntityIn<O> = O extends { readonly entity: infer Entity } ? Entity : never;\n\ntype ListFieldsIn<O, Key extends \"filter\" | \"sort\"> = O extends {\n readonly list: { readonly [K in Key]: readonly (infer Field extends string)[] };\n}\n ? Field\n : never;\n\ntype ListItemIn<O> = O extends { readonly list: { readonly item: infer Item } }\n ? RowOf<Item>\n : RowOf<EntityIn<O>>;\n\ntype InputIn<O, Name extends \"create\" | \"update\" | \"bulkUpdate\"> = O extends {\n readonly [K in Name]: { readonly input: infer Input extends StandardSchemaV1 };\n}\n ? Input\n : never;\n\ntype CrudMethodOf<O, Name extends CrudMethodName> = {\n get: CrudGet;\n getMany: CrudGetMany;\n list: CrudList<\n RowOf<EntityIn<O>>,\n ListItemIn<O>,\n ListFieldsIn<O, \"filter\">,\n ListFieldsIn<O, \"sort\">\n >;\n create: CrudCreate<InputIn<O, \"create\">>;\n update: CrudUpdate<InputIn<O, \"update\">>;\n delete: CrudDelete;\n reorder: CrudReorder;\n bulkUpdate: CrudBulkUpdate<InputIn<O, \"bulkUpdate\">>;\n bulkDelete: CrudBulkDelete;\n}[Name];\n\ntype Enabled<O, Name extends CrudMethodName> = Name extends keyof O\n ? [O[Name]] extends [false | undefined]\n ? never\n : Name\n : never;\n\n/** The entries `crud.contract(options)` returns: one per method the options turn on. */\nexport type CrudMethods<O> = {\n readonly [Name in CrudMethodName as Enabled<O, Name>]: CrudMethodOf<O, Name>;\n};\n\n/** What the server half needs to know about one of the kit's methods. */\nexport type CrudSpec =\n | { readonly method: Exclude<CrudMethodName, \"list\" | \"reorder\"> }\n | {\n readonly method: \"list\";\n readonly filter: readonly string[];\n readonly sort: readonly string[];\n /** The item's schema; `undefined` for the entity. */\n readonly item: RowSchema | undefined;\n }\n | { readonly method: \"reorder\"; readonly column: string; readonly within: readonly string[] };\n\nconst SPECS = new WeakMap<object, CrudSpec>();\n\n/** What the read/write kit made `method` for, or `undefined` for any other method. */\nexport function crudSpecOf(method: unknown): CrudSpec | undefined {\n return typeof method === \"object\" && method !== null ? SPECS.get(method) : undefined;\n}\n\nconst DESCRIBE: Readonly<Record<CrudMethodName, string>> = Object.freeze({\n get: \"Reads one row by id.\",\n getMany: `Reads up to ${CRUD_MAX_IDS} rows by id, in the order asked; ids the caller cannot read, or with no row, are left out.`,\n list: \"Lists the rows the caller can read, a page at a time, filtered and sorted by the declared fields. Pass a page's nextCursor back as cursor for the next page.\",\n create: \"Creates a row.\",\n update: \"Changes the given fields of one row.\",\n delete: \"Deletes one row.\",\n reorder:\n \"Moves a row between two others: beforeId is the row that will come right before it, afterId the row right after; one is enough.\",\n bulkUpdate: `Changes the same fields on up to ${CRUD_MAX_IDS} rows, skipping rows the caller cannot change; returns how many changed.`,\n bulkDelete: `Deletes up to ${CRUD_MAX_IDS} rows, skipping rows the caller cannot delete; returns how many were deleted.`,\n});\n\ntype UnknownRecord = Readonly<Record<string, unknown>>;\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction fail(message: string): never {\n throw new TypeError(`crud.contract: ${message}`);\n}\n\nfunction checkKeys(owner: string, value: UnknownRecord, allowed: readonly string[]): void {\n const unknownKey = Object.keys(value).find((key) => !allowed.includes(key));\n if (unknownKey !== undefined) {\n fail(`${owner} has an unknown option \"${unknownKey}\"; the options are ${allowed.join(\", \")}`);\n }\n}\n\n/** The method's description: its own `describe`, or the kit's. */\nfunction describeOf(name: CrudMethodName, option: unknown): string {\n const own = isRecord(option) ? option.describe : undefined;\n if (own !== undefined && (typeof own !== \"string\" || own.length === 0)) {\n fail(`${name}: describe must be a non-empty string`);\n }\n return own ?? DESCRIBE[name];\n}\n\nfunction checkToggle(name: CrudMethodName, option: unknown): void {\n if (typeof option !== \"boolean\" && !isRecord(option)) {\n fail(`${name} must be true, false or { describe }`);\n }\n if (isRecord(option)) {\n checkKeys(name, option, [\"describe\"]);\n }\n}\n\nfunction checkFields(owner: string, fields: unknown): readonly string[] {\n if (fields === undefined) {\n return [];\n }\n const valid =\n Array.isArray(fields) &&\n fields.every((field) => typeof field === \"string\" && field.length > 0) &&\n new Set(fields).size === fields.length;\n if (!valid) {\n fail(`${owner} must be a list of distinct field names`);\n }\n return Object.freeze([...(fields as string[])]);\n}\n\nfunction listSpec(option: unknown): CrudSpec {\n if (!isRecord(option)) {\n fail(\"list must be { item?, filter?, sort?, describe? }\");\n }\n checkKeys(\"list\", option, [\"item\", \"filter\", \"sort\", \"describe\"]);\n if (option.item !== undefined && !isStandardSchema(option.item)) {\n fail(\"list.item must be the entity schema or one of the contract's projection schemas\");\n }\n return {\n method: \"list\",\n filter: checkFields(\"list.filter\", option.filter),\n sort: checkFields(\"list.sort\", option.sort),\n item: option.item as RowSchema | undefined,\n };\n}\n\nfunction inputOf(name: CrudMethodName, option: unknown): StandardSchemaV1 {\n if (!isRecord(option) || !isStandardSchema(option.input)) {\n fail(`${name} must be { input: <Standard Schema>, describe? }`);\n }\n checkKeys(name, option, [\"input\", \"describe\"]);\n return option.input;\n}\n\nfunction reorderSpec(option: unknown): CrudSpec {\n if (!isRecord(option) || typeof option.column !== \"string\" || option.column.length === 0) {\n fail(\"reorder must be { column, within?, describe? } with the name of a numeric column\");\n }\n checkKeys(\"reorder\", option, [\"column\", \"within\", \"describe\"]);\n const { column } = option;\n const within = checkFields(\n \"reorder.within\",\n typeof option.within === \"string\" ? [option.within] : option.within,\n );\n if (column === \"id\" || within.includes(column)) {\n fail(\"reorder.column must be a numeric column other than id and the within columns\");\n }\n return { method: \"reorder\", column, within };\n}\n\n/** One method of the kit: its entry, and what the server half needs to know about it. */\nfunction build(\n name: CrudMethodName,\n options: UnknownRecord,\n entity: StandardSchemaV1,\n): { def: MethodDef; spec: CrudSpec } {\n const option = options[name];\n const describe = describeOf(name, option);\n switch (name) {\n case \"list\": {\n const spec = listSpec(option) as Extract<CrudSpec, { method: \"list\" }>;\n const input = listInput(spec, entity);\n return { def: query({ input, output: pageOutput(spec.item ?? entity), describe }), spec };\n }\n case \"create\":\n return {\n def: mutation({ input: inputOf(name, option), output: \"entity\", describe }),\n spec: { method: name },\n };\n case \"update\":\n return {\n def: mutation({ input: withId(inputOf(name, option)), output: \"entity\", describe }),\n spec: { method: name },\n };\n case \"bulkUpdate\":\n return {\n def: mutation({ input: bulkPatch(inputOf(name, option)), output: countOutput(), describe }),\n spec: { method: name },\n };\n case \"reorder\":\n return {\n def: mutation({ input: reorderInput(), output: \"entity\", describe }),\n spec: reorderSpec(option),\n };\n default:\n checkToggle(name, option);\n return { def: toggled(name, describe), spec: { method: name } };\n }\n}\n\n/** The entry of a method without options. */\nfunction toggled(name: CrudMethodName, describe: string): MethodDef {\n switch (name) {\n case \"get\":\n return query({ input: idInput(), output: \"entity\", describe });\n case \"getMany\":\n return query({ input: idsInput(), output: listOf(\"entity\"), describe });\n case \"delete\":\n return mutation({ input: idInput(), output: nullOutput(), describe });\n default:\n return mutation({ input: idsInput(), output: countOutput(), describe });\n }\n}\n\nfunction checkOptions(options: unknown): UnknownRecord {\n if (!isRecord(options)) {\n fail(\"options must be { entity, ...methods }\");\n }\n checkKeys(\"the options\", options, [\"entity\", ...CRUD_METHODS]);\n if (!isStandardSchema(options.entity)) {\n fail(\"entity must be the contract's entity schema\");\n }\n return options;\n}\n\nfunction contract<const O extends CrudContractOptions>(\n options: O & NoInfer<CrudChecks<O>>,\n): CrudMethods<O> {\n const checked = checkOptions(options);\n const entity = checked.entity as StandardSchemaV1;\n const methods: Record<string, MethodDef> = {};\n for (const name of CRUD_METHODS) {\n const option = checked[name];\n if (option === undefined || option === false) {\n continue;\n }\n const { def, spec } = build(name, checked, entity);\n SPECS.set(def, Object.freeze(spec));\n methods[name] = def;\n }\n return Object.freeze(methods) as unknown as CrudMethods<O>;\n}\n\n/**\n * The read/write kit's contract half: `crud.contract(options)`. Each method is\n * opt-in by name (`get: true`, `list: { filter, sort }`, `update: { input }`),\n * and the server half (`crud.handlers` on `./server`) implements exactly the\n * methods named.\n */\nexport const crud = Object.freeze({ contract });\n","// The inputs and outputs the sharing and membership kit generates (RFC 0003\n// section 12.3). An access list method names its row as `id` and its user as\n// `userId`, and grants one of the levels a list entry can usefully hold\n// (`Read`, `Moderate` or `Admin`); a membership method names its row as\n// `entryId` and a stored role as `role`, which the server checks against the\n// service's `members` policy (the contract cannot know its roles). The\n// by-name methods take `name` or `email` instead of `userId`, for the\n// server's `resolveUser`. `listMembers` pages like `list` (`crudList.ts`):\n// 50 members by default, at most 200, by keyset cursor.\n//\n// Outputs: an access list as `ACL` (`[{ userId, level }]`, one entry per\n// user), a member as `{ userId, role, level }`, a page of members, or `null`.\n\nimport { ACCESS_LEVELS, isAccessLevel, type ACL, type AccessLevel } from \"../access\";\nimport type { StandardSchemaV1 } from \"../standardSchema\";\nimport { LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT, MAX_CURSOR_LENGTH, pagingIssues } from \"./crudList\";\nimport {\n idJson,\n isId,\n isRecord,\n kitSchema,\n objectJson,\n unknownKeys,\n type JsonSchema,\n type KitSchema,\n} from \"./schemas\";\n\n/** A level an access list entry is given: `Public` grants nothing, so it is not one. */\nexport type ShareLevel = Exclude<AccessLevel, \"Public\">;\n\n/** The levels `share` and `setLevel` grant, lowest first. */\nexport const SHARE_LEVELS: readonly ShareLevel[] = Object.freeze([\"Read\", \"Moderate\", \"Admin\"]);\n\n/** The longest role, name or email the kit accepts. */\nconst MAX_TEXT_LENGTH = 256;\n\n/** `share` and `setLevel`: give `userId` `level` on the row `id`. */\nexport interface ShareInput {\n readonly id: string;\n readonly userId: string;\n readonly level: ShareLevel;\n}\n\n/** `unshare`: take `userId`'s level on the row `id` away. */\nexport interface UnshareInput {\n readonly id: string;\n readonly userId: string;\n}\n\n/** Who a by-name method means: their name or their email, at least one. */\nexport interface UserLookupInput {\n readonly name?: string;\n readonly email?: string;\n}\n\n/** `shareByName`: `share`, with the user found by name or email. */\nexport interface ShareByNameInput extends UserLookupInput {\n readonly id: string;\n readonly level: ShareLevel;\n}\n\n/** `shareByName`'s input as its handler receives it. */\nexport interface ShareByNameQuery {\n readonly id: string;\n readonly name: string | undefined;\n readonly email: string | undefined;\n readonly level: ShareLevel;\n}\n\n/** `invite`: make `userId` a member of the row `entryId`, with `role` (the lowest by default). */\nexport interface InviteInput {\n readonly entryId: string;\n readonly userId: string;\n readonly role?: string;\n}\n\n/** `invite`'s input as its handler receives it. */\nexport interface InviteQuery {\n readonly entryId: string;\n readonly userId: string;\n readonly role: string | undefined;\n}\n\n/** `inviteByName`: `invite`, with the user found by name or email. */\nexport interface InviteByNameInput extends UserLookupInput {\n readonly entryId: string;\n readonly role?: string;\n}\n\n/** `inviteByName`'s input as its handler receives it. */\nexport interface InviteByNameQuery {\n readonly entryId: string;\n readonly name: string | undefined;\n readonly email: string | undefined;\n readonly role: string | undefined;\n}\n\n/** `remove`: end `userId`'s membership of the row `entryId`. */\nexport interface MemberInput {\n readonly entryId: string;\n readonly userId: string;\n}\n\n/** `leave`: end the caller's own membership of the row `entryId`. */\nexport interface LeaveInput {\n readonly entryId: string;\n}\n\n/** `setRole`: change `userId`'s role on the row `entryId`. */\nexport interface SetRoleInput {\n readonly entryId: string;\n readonly userId: string;\n readonly role: string;\n}\n\n/** `listMembers`: one page of the members of the row `entryId`. */\nexport interface ListMembersInput {\n readonly entryId: string;\n /** The `nextCursor` of the page before; the first page when absent. */\n readonly cursor?: string;\n /** The page size: default 50, at most 200 (a larger one is clamped). */\n readonly limit?: number;\n}\n\n/** `listMembers`' input as its handler receives it: the limit defaulted and clamped. */\nexport interface ListMembersQuery {\n readonly entryId: string;\n readonly cursor: string | undefined;\n readonly limit: number;\n}\n\n/** One member of a row. */\nexport interface Member {\n readonly userId: string;\n /** The role the membership table stores. */\n readonly role: string;\n /** The access level the role gives, or `null` for a role that gives none. */\n readonly level: AccessLevel | null;\n}\n\n/** One page of `listMembers`. */\nexport interface MembersPage {\n readonly items: Member[];\n /** Passed back as `cursor` for the next page; `null` on the last page. */\n readonly nextCursor: string | null;\n}\n\ntype Issues = StandardSchemaV1.Issue[];\n\n/** What one input key holds: a row or user id, a level, a role, or a name or email. */\ntype FieldKind = \"id\" | \"level\" | \"role\" | \"text\";\n\ninterface Field {\n readonly kind: FieldKind;\n readonly optional?: boolean;\n}\n\nconst MESSAGES: Readonly<Record<FieldKind, string>> = Object.freeze({\n id: \"Expected a non-empty string\",\n level: 'Expected \"Read\", \"Moderate\" or \"Admin\"',\n role: `Expected a role: a non-empty string of at most ${MAX_TEXT_LENGTH} characters`,\n text: `Expected a non-empty string of at most ${MAX_TEXT_LENGTH} characters`,\n});\n\nfunction isText(value: unknown): boolean {\n return typeof value === \"string\" && value.length > 0 && value.length <= MAX_TEXT_LENGTH;\n}\n\nfunction fits(kind: FieldKind, value: unknown): boolean {\n if (kind === \"id\") {\n return isId(value);\n }\n return kind === \"level\" ? SHARE_LEVELS.some((level) => level === value) : isText(value);\n}\n\nfunction fieldJson(kind: FieldKind): JsonSchema {\n if (kind === \"id\") {\n return idJson();\n }\n return kind === \"level\"\n ? { type: \"string\", enum: [...SHARE_LEVELS] }\n : { type: \"string\", minLength: 1, maxLength: MAX_TEXT_LENGTH };\n}\n\n/** How one input is checked and parsed. */\ninterface InputShape {\n readonly fields: Readonly<Record<string, Field>>;\n /** Keys checked by `issues` rather than by a field: `listMembers`' paging. */\n readonly more?: Readonly<Record<string, JsonSchema>>;\n /** Issues no single field shows: a by-name method needs a name or an email. */\n readonly issues?: (value: Readonly<Record<string, unknown>>) => Issues;\n /** The parsed input, when it is not the known keys as given. */\n readonly parse?: (value: Readonly<Record<string, unknown>>) => unknown;\n}\n\nfunction fieldIssues(shape: InputShape, value: Readonly<Record<string, unknown>>): Issues {\n return Object.entries(shape.fields).flatMap(([key, field]): Issues => {\n const given = value[key];\n const missing = given === undefined && field.optional !== true;\n const wrong = given !== undefined && !fits(field.kind, given);\n return missing || wrong ? [{ message: MESSAGES[field.kind], path: [key] }] : [];\n });\n}\n\nfunction inputJson(shape: InputShape): JsonSchema {\n const properties = Object.fromEntries(\n Object.entries(shape.fields).map(([key, field]) => [key, fieldJson(field.kind)]),\n );\n const required = Object.entries(shape.fields)\n .filter(([, field]) => field.optional !== true)\n .map(([key]) => key);\n return objectJson({ ...properties, ...shape.more }, required);\n}\n\n/** An input made of `shape`'s keys and nothing else. */\nfunction objectInput<Input, Parsed = Input>(shape: InputShape): KitSchema<Input, Parsed> {\n const keys = [...Object.keys(shape.fields), ...Object.keys(shape.more ?? {})];\n return kitSchema<Input, Parsed>(\n (input) => {\n if (!isRecord(input)) {\n return { issues: [{ message: \"Expected an object\", path: [] }] };\n }\n const issues = [\n ...unknownKeys(input, keys),\n ...fieldIssues(shape, input),\n ...(shape.issues?.(input) ?? []),\n ];\n if (issues.length > 0) {\n return { issues };\n }\n const parsed =\n shape.parse?.(input) ?? Object.fromEntries(keys.map((key) => [key, input[key]]));\n return { value: parsed as Parsed };\n },\n { input: () => inputJson(shape) },\n );\n}\n\nconst ID: Field = { kind: \"id\" };\nconst LEVEL: Field = { kind: \"level\" };\nconst ROLE: Field = { kind: \"role\" };\nconst OPTIONAL_ROLE: Field = { kind: \"role\", optional: true };\nconst OPTIONAL_TEXT: Field = { kind: \"text\", optional: true };\n\nfunction lookupIssues(value: Readonly<Record<string, unknown>>): Issues {\n return value.name === undefined && value.email === undefined\n ? [{ message: \"Give name or email\", path: [] }]\n : [];\n}\n\n/** `{ id, userId, level }`: `share`'s and `setLevel`'s input. */\nexport function shareInput(): KitSchema<ShareInput> {\n return objectInput<ShareInput>({ fields: { id: ID, userId: ID, level: LEVEL } });\n}\n\n/** `{ id, name?, email?, level }`: `shareByName`'s input. */\nexport function shareByNameInput(): KitSchema<ShareByNameInput, ShareByNameQuery> {\n return objectInput<ShareByNameInput, ShareByNameQuery>({\n fields: { id: ID, name: OPTIONAL_TEXT, email: OPTIONAL_TEXT, level: LEVEL },\n issues: lookupIssues,\n });\n}\n\n/** `{ id, userId }`: `unshare`'s input. */\nexport function unshareInput(): KitSchema<UnshareInput> {\n return objectInput<UnshareInput>({ fields: { id: ID, userId: ID } });\n}\n\n/** `{ entryId, userId, role? }`: `invite`'s input. */\nexport function inviteInput(): KitSchema<InviteInput, InviteQuery> {\n return objectInput<InviteInput, InviteQuery>({\n fields: { entryId: ID, userId: ID, role: OPTIONAL_ROLE },\n });\n}\n\n/** `{ entryId, name?, email?, role? }`: `inviteByName`'s input. */\nexport function inviteByNameInput(): KitSchema<InviteByNameInput, InviteByNameQuery> {\n return objectInput<InviteByNameInput, InviteByNameQuery>({\n fields: { entryId: ID, name: OPTIONAL_TEXT, email: OPTIONAL_TEXT, role: OPTIONAL_ROLE },\n issues: lookupIssues,\n });\n}\n\n/** `{ entryId, userId }`: `remove`'s input. */\nexport function memberInput(): KitSchema<MemberInput> {\n return objectInput<MemberInput>({ fields: { entryId: ID, userId: ID } });\n}\n\n/** `{ entryId }`: `leave`'s input. */\nexport function leaveInput(): KitSchema<LeaveInput> {\n return objectInput<LeaveInput>({ fields: { entryId: ID } });\n}\n\n/** `{ entryId, userId, role }`: `setRole`'s input. */\nexport function setRoleInput(): KitSchema<SetRoleInput> {\n return objectInput<SetRoleInput>({ fields: { entryId: ID, userId: ID, role: ROLE } });\n}\n\n/** `{ entryId, cursor?, limit? }`: `listMembers`' input. */\nexport function listMembersInput(): KitSchema<ListMembersInput, ListMembersQuery> {\n return objectInput<ListMembersInput, ListMembersQuery>({\n fields: { entryId: ID },\n more: {\n cursor: { type: \"string\", minLength: 1, maxLength: MAX_CURSOR_LENGTH },\n limit: { type: \"integer\", minimum: 1, maximum: LIST_MAX_LIMIT, default: LIST_DEFAULT_LIMIT },\n },\n issues: (value) => pagingIssues({ cursor: value.cursor, limit: value.limit }),\n parse: (value): ListMembersQuery => ({\n entryId: value.entryId as string,\n cursor: value.cursor as string | undefined,\n limit: Math.min((value.limit as number | undefined) ?? LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT),\n }),\n });\n}\n\nfunction shareIssues(entry: unknown, path: readonly PropertyKey[]): Issues {\n const valid =\n isRecord(entry) &&\n isId(entry.userId) &&\n isAccessLevel(entry.level) &&\n Object.keys(entry).length === 2;\n return valid ? [] : [{ message: \"Expected { userId, level }\", path: [...path] }];\n}\n\nfunction shareJson(): JsonSchema {\n return objectJson({ userId: idJson(), level: { type: \"string\", enum: [...ACCESS_LEVELS] } }, [\n \"userId\",\n \"level\",\n ]);\n}\n\n/** `[{ userId, level }]`: an access list, as `share`, `unshare`, `setLevel` and `listShares` return it. */\nexport function aclOutput(): KitSchema<ACL> {\n return kitSchema<ACL>(\n (value) => {\n if (!Array.isArray(value)) {\n return { issues: [{ message: \"Expected an access list\", path: [] }] };\n }\n const issues = value.flatMap((entry: unknown, index) => shareIssues(entry, [index]));\n return issues.length > 0 ? { issues } : { value: value as ACL };\n },\n { input: () => ({ type: \"array\", items: shareJson() }) },\n );\n}\n\nfunction memberIssues(member: unknown, path: readonly PropertyKey[]): Issues {\n const valid =\n isRecord(member) &&\n isId(member.userId) &&\n typeof member.role === \"string\" &&\n (member.level === null || isAccessLevel(member.level)) &&\n Object.keys(member).length === 3;\n return valid ? [] : [{ message: \"Expected { userId, role, level }\", path: [...path] }];\n}\n\nfunction memberJson(): JsonSchema {\n return objectJson(\n { userId: idJson(), role: { type: \"string\" }, level: { enum: [...ACCESS_LEVELS, null] } },\n [\"userId\", \"role\", \"level\"],\n );\n}\n\n/** `{ userId, role, level }`: a member, as `invite` and `setRole` return it. */\nexport function memberOutput(): KitSchema<Member> {\n return kitSchema<Member>(\n (value) => {\n const issues = memberIssues(value, []);\n return issues.length > 0 ? { issues } : { value: value as Member };\n },\n { input: memberJson },\n );\n}\n\n/** `{ items, nextCursor }`: one page of `listMembers`. */\nexport function membersPageOutput(): KitSchema<MembersPage> {\n return kitSchema<MembersPage>(\n (value) => {\n if (!isRecord(value) || !Array.isArray(value.items)) {\n return { issues: [{ message: \"Expected a page of members\", path: [] }] };\n }\n const issues = [\n ...unknownKeys(value, [\"items\", \"nextCursor\"]),\n ...value.items.flatMap((member: unknown, index) => memberIssues(member, [\"items\", index])),\n ];\n if (value.nextCursor !== null && typeof value.nextCursor !== \"string\") {\n issues.push({ message: \"Expected a cursor or null\", path: [\"nextCursor\"] });\n }\n return issues.length > 0 ? { issues } : { value: value as unknown as MembersPage };\n },\n {\n input: () =>\n objectJson(\n {\n items: { type: \"array\", items: memberJson() },\n nextCursor: { type: [\"string\", \"null\"] },\n },\n [\"items\", \"nextCursor\"],\n ),\n },\n );\n}\n","// The sharing and membership kit's contract half (RFC 0003 section 12.3).\n// `sharing.contract` returns ordinary `query` and `mutation` entries to\n// spread into a contract's `methods`, for one of two ways a service's rows\n// are shared:\n//\n// export const project = defineContract(\"projectService\", {\n// entity: projectSchema,\n// methods: {\n// ...sharing.contract({ mode: \"acl\" }), // share, unshare, setLevel, listShares\n// ...sharing.contract({ mode: \"members\", methods: [\"invite\", \"remove\", \"leave\", \"listMembers\"] }),\n// },\n// });\n//\n// `\"acl\"` edits the JSON access list a `jsonAcl` policy reads; `\"members\"`\n// edits the membership table a `members` policy reads. `methods` picks the\n// methods to add, by name; without it a mode adds all of its methods but the\n// by-name ones (`shareByName`, `inviteByName`), which find the user by name\n// or email through the server's `resolveUser`. The server half\n// (`sharing.handlers` on `./server`) finds the kit's entries in the contract\n// by what they were made for (`sharingSpecOf`), and its types by their tag\n// (`SharingTag`), so the two halves name the same methods.\n\nimport type { ACL } from \"../access\";\nimport { mutation, query, type MethodDef, type MutationDef, type QueryDef } from \"../methods\";\nimport { idInput, nullOutput, type IdInput } from \"./crudSchemas\";\nimport type { KitSchema } from \"./schemas\";\nimport {\n aclOutput,\n inviteByNameInput,\n inviteInput,\n leaveInput,\n listMembersInput,\n memberInput,\n memberOutput,\n membersPageOutput,\n setRoleInput,\n shareByNameInput,\n shareInput,\n unshareInput,\n type InviteByNameInput,\n type InviteByNameQuery,\n type InviteInput,\n type InviteQuery,\n type LeaveInput,\n type ListMembersInput,\n type ListMembersQuery,\n type Member,\n type MemberInput,\n type MembersPage,\n type SetRoleInput,\n type ShareByNameInput,\n type ShareByNameQuery,\n type ShareInput,\n type UnshareInput,\n} from \"./sharingSchemas\";\n\n/** How a service's rows are shared: a JSON access list (`jsonAcl`) or a membership table (`members`). */\nexport type SharingMode = \"acl\" | \"members\";\n\n/** The methods of `mode: \"acl\"`. */\nexport type AclMethodName = \"share\" | \"shareByName\" | \"unshare\" | \"setLevel\" | \"listShares\";\n\n/** The methods of `mode: \"members\"`. */\nexport type MembersMethodName =\n | \"invite\"\n | \"inviteByName\"\n | \"remove\"\n | \"leave\"\n | \"setRole\"\n | \"listMembers\";\n\n/** Every method the sharing kit can add. */\nexport type SharingMethodName = AclMethodName | MembersMethodName;\n\n/** The methods that find their user by name or email: opt-in, since they need `resolveUser`. */\nexport type SharingByNameMethod = \"shareByName\" | \"inviteByName\";\n\n/** Every method of each mode: those of RFC 0003 section 12.3, then the by-name one. */\nexport const SHARING_METHODS: {\n readonly acl: readonly AclMethodName[];\n readonly members: readonly MembersMethodName[];\n} = Object.freeze({\n acl: Object.freeze<AclMethodName[]>([\n \"share\",\n \"unshare\",\n \"setLevel\",\n \"listShares\",\n \"shareByName\",\n ]),\n members: Object.freeze<MembersMethodName[]>([\n \"invite\",\n \"remove\",\n \"leave\",\n \"setRole\",\n \"listMembers\",\n \"inviteByName\",\n ]),\n});\n\n/** Type-only: marks a method the sharing kit made, and which one. Never set. */\nexport interface SharingTag<Kind extends SharingMethodName> {\n readonly \"~sharing\"?: Kind;\n}\n\n/** `sharing.contract`'s options, for one mode. */\nexport type SharingContractOptions =\n | {\n readonly mode: \"acl\";\n /** The methods to add; all but `shareByName` when absent. */\n readonly methods?: readonly AclMethodName[];\n /** Descriptions for people and agents, per method; the kit's own for the rest. */\n readonly describe?: { readonly [Name in AclMethodName]?: string };\n }\n | {\n readonly mode: \"members\";\n /** The methods to add; all but `inviteByName` when absent. */\n readonly methods?: readonly MembersMethodName[];\n /** Descriptions for people and agents, per method; the kit's own for the rest. */\n readonly describe?: { readonly [Name in MembersMethodName]?: string };\n };\n\nexport type SharingShare = MutationDef<KitSchema<ShareInput>, KitSchema<ACL>> & SharingTag<\"share\">;\nexport type SharingShareByName = MutationDef<\n KitSchema<ShareByNameInput, ShareByNameQuery>,\n KitSchema<ACL>\n> &\n SharingTag<\"shareByName\">;\nexport type SharingUnshare = MutationDef<KitSchema<UnshareInput>, KitSchema<ACL>> &\n SharingTag<\"unshare\">;\nexport type SharingSetLevel = MutationDef<KitSchema<ShareInput>, KitSchema<ACL>> &\n SharingTag<\"setLevel\">;\nexport type SharingListShares = QueryDef<KitSchema<IdInput>, KitSchema<ACL>> &\n SharingTag<\"listShares\">;\nexport type SharingInvite = MutationDef<KitSchema<InviteInput, InviteQuery>, KitSchema<Member>> &\n SharingTag<\"invite\">;\nexport type SharingInviteByName = MutationDef<\n KitSchema<InviteByNameInput, InviteByNameQuery>,\n KitSchema<Member>\n> &\n SharingTag<\"inviteByName\">;\nexport type SharingRemove = MutationDef<KitSchema<MemberInput>, KitSchema<null>> &\n SharingTag<\"remove\">;\nexport type SharingLeave = MutationDef<KitSchema<LeaveInput>, KitSchema<null>> &\n SharingTag<\"leave\">;\nexport type SharingSetRole = MutationDef<KitSchema<SetRoleInput>, KitSchema<Member>> &\n SharingTag<\"setRole\">;\nexport type SharingListMembers = QueryDef<\n KitSchema<ListMembersInput, ListMembersQuery>,\n KitSchema<MembersPage>\n> &\n SharingTag<\"listMembers\">;\n\n/** The entry `sharing.contract` makes for method `Name`. */\nexport type SharingDefOf<Name extends SharingMethodName> = {\n share: SharingShare;\n shareByName: SharingShareByName;\n unshare: SharingUnshare;\n setLevel: SharingSetLevel;\n listShares: SharingListShares;\n invite: SharingInvite;\n inviteByName: SharingInviteByName;\n remove: SharingRemove;\n leave: SharingLeave;\n setRole: SharingSetRole;\n listMembers: SharingListMembers;\n}[Name];\n\ntype ModeIn<O> = O extends { readonly mode: infer Mode } ? Mode : never;\n\ntype DefaultNames<Mode> = Mode extends \"acl\"\n ? Exclude<AclMethodName, SharingByNameMethod>\n : Mode extends \"members\"\n ? Exclude<MembersMethodName, SharingByNameMethod>\n : never;\n\ntype NamesIn<O> = O extends { readonly methods: readonly (infer Name extends SharingMethodName)[] }\n ? Name\n : DefaultNames<ModeIn<O>>;\n\n/** The entries `sharing.contract(options)` returns: one per method it adds. */\nexport type SharingMethods<O> = { readonly [Name in NamesIn<O>]: SharingDefOf<Name> };\n\n/** What the server half needs to know about a method the sharing kit made. */\nexport interface SharingSpec {\n readonly method: SharingMethodName;\n readonly mode: SharingMode;\n}\n\nconst SPECS = new WeakMap<object, SharingSpec>();\n\n/** What the sharing kit made `method` for, or `undefined` for any other method. */\nexport function sharingSpecOf(method: unknown): SharingSpec | undefined {\n return typeof method === \"object\" && method !== null ? SPECS.get(method) : undefined;\n}\n\nconst DESCRIBE: Readonly<Record<SharingMethodName, string>> = Object.freeze({\n share:\n \"Gives a user a level on one row: Read, Moderate or Admin. A user the row is shared with already gets the new level, unless theirs is above yours.\",\n shareByName:\n \"Gives the user this name or email finds a level on one row: Read, Moderate or Admin. Use the email when several users may share the name.\",\n unshare:\n \"Takes a user's level on one row away. The owner's cannot be taken away, nor that of a user whose level is above yours.\",\n setLevel:\n \"Changes the level of a user one row is already shared with, unless their level is above yours.\",\n listShares: \"Lists the users one row is shared with, and their levels.\",\n invite: \"Makes a user a member of one row, with a role (the lowest role when none is given).\",\n inviteByName:\n \"Makes the user this name or email finds a member of one row, with a role (the lowest role when none is given). Use the email when several users may share the name.\",\n remove:\n \"Ends a user's membership of one row. The last Admin member cannot be removed, nor a member whose level is above yours.\",\n leave: \"Ends the caller's own membership of one row. The last Admin member cannot leave.\",\n setRole: \"Changes a member's role on one row, unless their level is above yours.\",\n listMembers:\n \"Lists the members of one row and their roles, a page at a time. Pass a page's nextCursor back as cursor for the next page.\",\n});\n\nconst BUILD: Readonly<Record<SharingMethodName, (describe: string) => MethodDef>> = Object.freeze({\n share: (describe) => mutation({ input: shareInput(), output: aclOutput(), describe }),\n shareByName: (describe) => mutation({ input: shareByNameInput(), output: aclOutput(), describe }),\n unshare: (describe) => mutation({ input: unshareInput(), output: aclOutput(), describe }),\n setLevel: (describe) => mutation({ input: shareInput(), output: aclOutput(), describe }),\n listShares: (describe) => query({ input: idInput(), output: aclOutput(), describe }),\n invite: (describe) => mutation({ input: inviteInput(), output: memberOutput(), describe }),\n inviteByName: (describe) =>\n mutation({ input: inviteByNameInput(), output: memberOutput(), describe }),\n remove: (describe) => mutation({ input: memberInput(), output: nullOutput(), describe }),\n leave: (describe) => mutation({ input: leaveInput(), output: nullOutput(), describe }),\n setRole: (describe) => mutation({ input: setRoleInput(), output: memberOutput(), describe }),\n listMembers: (describe) =>\n query({ input: listMembersInput(), output: membersPageOutput(), describe }),\n});\n\ntype UnknownRecord = Readonly<Record<string, unknown>>;\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction fail(message: string): never {\n throw new TypeError(`sharing.contract: ${message}`);\n}\n\nfunction quoted(names: readonly string[]): string {\n return names.map((name) => `\"${name}\"`).join(\", \");\n}\n\nfunction modeOf(options: unknown): SharingMode {\n if (!isRecord(options)) {\n fail('options must be { mode: \"acl\" | \"members\", methods?, describe? }');\n }\n const unknownKey = Object.keys(options).find(\n (key) => ![\"mode\", \"methods\", \"describe\"].includes(key),\n );\n if (unknownKey !== undefined) {\n fail(`unknown option \"${unknownKey}\"; the options are mode, methods and describe`);\n }\n const { mode } = options;\n if (mode !== \"acl\" && mode !== \"members\") {\n fail('mode must be \"acl\" (a jsonAcl access list) or \"members\" (a members table)');\n }\n return mode;\n}\n\n/** The methods the options add: those `methods` names, or the mode's default ones. */\nfunction namesOf(mode: SharingMode, methods: unknown): readonly SharingMethodName[] {\n const all: readonly SharingMethodName[] = SHARING_METHODS[mode];\n if (methods === undefined) {\n return all.filter((name) => name !== \"shareByName\" && name !== \"inviteByName\");\n }\n const valid =\n Array.isArray(methods) &&\n methods.length > 0 &&\n methods.every((name) => all.some((known) => known === name)) &&\n new Set(methods).size === methods.length;\n if (!valid) {\n fail(`methods must name one or more distinct methods of mode \"${mode}\": ${quoted(all)}`);\n }\n return methods as SharingMethodName[];\n}\n\n/** The descriptions the options give, each for a method they add. */\nfunction describeOf(names: readonly SharingMethodName[], describe: unknown): UnknownRecord {\n if (describe === undefined) {\n return {};\n }\n if (!isRecord(describe)) {\n fail(\"describe must map method names to descriptions\");\n }\n for (const [name, text] of Object.entries(describe)) {\n if (!names.some((added) => added === name)) {\n fail(`describe names \"${name}\", which these options do not add`);\n }\n if (typeof text !== \"string\" || text.length === 0) {\n fail(`describe for \"${name}\" must be a non-empty string`);\n }\n }\n return describe;\n}\n\nfunction contract<const O extends SharingContractOptions>(options: O): SharingMethods<O> {\n const mode = modeOf(options);\n const names = namesOf(mode, options.methods);\n const described = describeOf(names, options.describe);\n const methods: Record<string, MethodDef> = {};\n for (const name of names) {\n const own = described[name];\n const def = BUILD[name](typeof own === \"string\" ? own : DESCRIBE[name]);\n SPECS.set(def, Object.freeze({ method: name, mode }));\n methods[name] = def;\n }\n return Object.freeze(methods) as unknown as SharingMethods<O>;\n}\n\n/**\n * The sharing and membership kit's contract half: `sharing.contract({ mode,\n * methods?, describe? })` makes the methods that share a row through its\n * JSON access list (`mode: \"acl\"`) or its membership table\n * (`mode: \"members\"`), which the server half (`sharing.handlers` on\n * `./server`) implements.\n */\nexport const sharing = Object.freeze({ contract });\n"],"mappings":"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","names":["isRecord","SPECS","DESCRIBE","isRecord","fail","describeOf","contract"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/server/auth/validateOrigin.ts
|
|
2
|
+
var OAUTH_RETURN_ORIGIN_COOKIE = "oauth_return_origin";
|
|
3
|
+
var LOCALHOST_REGEX = /^http:\/\/localhost:\d+$/;
|
|
4
|
+
function envExtraOrigins() {
|
|
5
|
+
const raw = process.env.EXTRA_ALLOWED_ORIGINS;
|
|
6
|
+
if (!raw) return [];
|
|
7
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
function validateRedirectOrigin(origin, options = {}) {
|
|
10
|
+
if (!origin) return null;
|
|
11
|
+
const clientUrl = options.clientUrl ?? process.env.CLIENT_URL;
|
|
12
|
+
let parsedOrigin;
|
|
13
|
+
try {
|
|
14
|
+
parsedOrigin = new URL(origin);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const cleanOrigin = parsedOrigin.origin;
|
|
19
|
+
if (clientUrl && cleanOrigin === clientUrl) {
|
|
20
|
+
return cleanOrigin;
|
|
21
|
+
}
|
|
22
|
+
const extra = options.extraAllowedOrigins ?? envExtraOrigins();
|
|
23
|
+
if (extra.includes(cleanOrigin)) {
|
|
24
|
+
return cleanOrigin;
|
|
25
|
+
}
|
|
26
|
+
if (options.allowedPatterns?.some((pattern) => pattern.test(cleanOrigin))) {
|
|
27
|
+
return cleanOrigin;
|
|
28
|
+
}
|
|
29
|
+
if ((options.allowLocalhostInDev ?? true) && process.env.NODE_ENV !== "production" && LOCALHOST_REGEX.test(cleanOrigin)) {
|
|
30
|
+
return cleanOrigin;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/server/auth/routes/origins.ts
|
|
36
|
+
function exactOrigin(entry, owner) {
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = typeof entry === "string" ? new URL(entry) : void 0;
|
|
40
|
+
} catch {
|
|
41
|
+
url = void 0;
|
|
42
|
+
}
|
|
43
|
+
const bare = url !== void 0 && (url.protocol === "https:" || url.protocol === "http:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
|
|
44
|
+
if (url === void 0 || !bare) {
|
|
45
|
+
throw new TypeError(
|
|
46
|
+
`${owner}: allowedOrigins entries are origins such as "https://app.example.com" or anchored patterns; got ${String(entry)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return url.origin;
|
|
50
|
+
}
|
|
51
|
+
function anchoredPattern(pattern, owner) {
|
|
52
|
+
const { source } = pattern;
|
|
53
|
+
const anchored = source.startsWith("^") && source.endsWith("$") && !source.endsWith("\\$");
|
|
54
|
+
if (!anchored || pattern.global || pattern.sticky) {
|
|
55
|
+
throw new TypeError(
|
|
56
|
+
`${owner}: an allowedOrigins pattern must match whole origins, anchored with ^ and $ and without the g or y flag; got ${String(pattern)}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return new RegExp(`^(?:${source})$`, pattern.flags);
|
|
60
|
+
}
|
|
61
|
+
function originAllowlist(allowedOrigins, owner, allowEmpty = false) {
|
|
62
|
+
if (!Array.isArray(allowedOrigins) || allowedOrigins.length === 0 && !allowEmpty) {
|
|
63
|
+
throw new TypeError(`${owner}: allowedOrigins must list the web app's origins`);
|
|
64
|
+
}
|
|
65
|
+
const exact = [];
|
|
66
|
+
const patterns = [];
|
|
67
|
+
for (const entry of allowedOrigins) {
|
|
68
|
+
if (entry instanceof RegExp) {
|
|
69
|
+
patterns.push(anchoredPattern(entry, owner));
|
|
70
|
+
} else {
|
|
71
|
+
exact.push(exactOrigin(entry, owner));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const options = {
|
|
75
|
+
// An empty client URL, not undefined: undefined falls back to process.env.CLIENT_URL.
|
|
76
|
+
clientUrl: "",
|
|
77
|
+
extraAllowedOrigins: exact,
|
|
78
|
+
allowedPatterns: patterns,
|
|
79
|
+
allowLocalhostInDev: false
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
allowed: (value) => typeof value === "string" && value !== "" ? validateRedirectOrigin(value, options) : null,
|
|
83
|
+
fallback: exact[0]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
var ROUTE_ORIGINS = /* @__PURE__ */ new WeakMap();
|
|
87
|
+
function rememberRouteOrigins(sessions, origins) {
|
|
88
|
+
ROUTE_ORIGINS.set(sessions, origins);
|
|
89
|
+
}
|
|
90
|
+
function routeOriginsOf(sessions) {
|
|
91
|
+
return ROUTE_ORIGINS.get(sessions);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
OAUTH_RETURN_ORIGIN_COOKIE,
|
|
96
|
+
validateRedirectOrigin,
|
|
97
|
+
originAllowlist,
|
|
98
|
+
rememberRouteOrigins,
|
|
99
|
+
routeOriginsOf
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=chunk-NO4M6PO5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/auth/validateOrigin.ts","../src/server/auth/routes/origins.ts"],"sourcesContent":["/**\n * Origin validation utility for OAuth redirect and CORS security.\n *\n * An origin is allowed when it is the client URL, one of the extra origins\n * or matches an app's pattern, or, outside production, is localhost. GitHub\n * Codespaces origins (`*.app.github.dev`) are never allowed by default since\n * 5.0.1 (finding R1.2 of the 5.0.0 review): anyone can open a Codespace, so\n * the allowance let any such page open a socket or finish a sign-in as a\n * signed-in user in production, wherever an app used this helper for CORS or\n * a cookie check. An app that wants one lists a pattern in `allowedPatterns`.\n */\n\nexport const OAUTH_RETURN_ORIGIN_COOKIE = \"oauth_return_origin\";\n\nexport interface ValidateOriginOptions {\n /**\n * The primary web client origin. Defaults to process.env.CLIENT_URL.\n */\n clientUrl?: string;\n /**\n * Additional exact-match origins. Defaults to the comma-separated\n * process.env.EXTRA_ALLOWED_ORIGINS. Lets a single API back multiple web\n * hosts (e.g. prod + staging).\n */\n extraAllowedOrigins?: string[];\n /**\n * App-specific origin patterns (e.g. preview-deploy subdomains).\n */\n allowedPatterns?: RegExp[];\n /**\n * Ignored: kept so 5.0.0 code that passes it still compiles.\n *\n * @deprecated Codespaces origins are never allowed since 5.0.1; list a pattern in allowedPatterns if one is wanted.\n */\n allowCodespaces?: boolean;\n /**\n * Allow http://localhost:* when NODE_ENV !== \"production\". Default: true.\n */\n allowLocalhostInDev?: boolean;\n}\n\nconst LOCALHOST_REGEX = /^http:\\/\\/localhost:\\d+$/;\n\nfunction envExtraOrigins(): string[] {\n const raw = process.env.EXTRA_ALLOWED_ORIGINS;\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Validates if an origin is safe for OAuth redirect / CORS.\n *\n * Only the origin (scheme + host + port) is compared, never the path, to\n * prevent path-injection bypasses.\n *\n * @param origin - The origin to validate\n * @param options - Override env-derived defaults\n * @returns The validated origin if allowed, null otherwise\n */\nexport function validateRedirectOrigin(\n origin: string,\n options: ValidateOriginOptions = {},\n): string | null {\n if (!origin) return null;\n\n const clientUrl = options.clientUrl ?? process.env.CLIENT_URL;\n\n let parsedOrigin: URL;\n try {\n parsedOrigin = new URL(origin);\n } catch {\n return null;\n }\n\n const cleanOrigin = parsedOrigin.origin;\n\n if (clientUrl && cleanOrigin === clientUrl) {\n return cleanOrigin;\n }\n\n const extra = options.extraAllowedOrigins ?? envExtraOrigins();\n if (extra.includes(cleanOrigin)) {\n return cleanOrigin;\n }\n\n if (options.allowedPatterns?.some((pattern) => pattern.test(cleanOrigin))) {\n return cleanOrigin;\n }\n\n if (\n (options.allowLocalhostInDev ?? true) &&\n process.env.NODE_ENV !== \"production\" &&\n LOCALHOST_REGEX.test(cleanOrigin)\n ) {\n return cleanOrigin;\n }\n\n return null;\n}\n","// The browser origins the auth kit trusts (RFC 0003 section 12.6): where a\n// sign-in may return to, and which pages may open a socket with the session\n// cookie. One list serves both. `validateRedirectOrigin` decides, with each\n// of its defaults turned off: no CLIENT_URL or EXTRA_ALLOWED_ORIGINS from the\n// environment, no implicit localhost (it allows no GitHub Codespaces origin\n// at all). The list the app passes is the whole allowlist.\n\nimport { validateRedirectOrigin, type ValidateOriginOptions } from \"../validateOrigin\";\n\n/**\n * An allowed origin: exact (`\"https://app.example.com\"`), or a pattern over\n * whole origins, anchored at both ends\n * (`/^https:\\/\\/[a-z0-9-]+\\.preview\\.example\\.com$/`).\n */\nexport type AllowedOrigin = string | RegExp;\n\n/** The allowlist, checked and normalized. */\nexport interface OriginAllowlist {\n /** The origin of `value` (an origin or a URL) when it is allowed, else `null`. */\n allowed(value: string | undefined): string | null;\n /** The first exact origin listed: where a sign-in returns when it names none. */\n readonly fallback: string | undefined;\n}\n\nfunction exactOrigin(entry: unknown, owner: string): string {\n let url: URL | undefined;\n try {\n url = typeof entry === \"string\" ? new URL(entry) : undefined;\n } catch {\n url = undefined;\n }\n const bare =\n url !== undefined &&\n (url.protocol === \"https:\" || url.protocol === \"http:\") &&\n url.username === \"\" &&\n url.password === \"\" &&\n url.pathname === \"/\" &&\n url.search === \"\" &&\n url.hash === \"\";\n if (url === undefined || !bare) {\n throw new TypeError(\n `${owner}: allowedOrigins entries are origins such as \"https://app.example.com\" or anchored patterns; got ${String(entry)}`,\n );\n }\n return url.origin;\n}\n\n/**\n * The pattern, refused unless it is written anchored with `^` and `$`, and\n * compiled to match whole origins whatever it holds: `^a|b$` is anchored\n * only at its ends (`^a` or `b$`), so `/^https:\\/\\/app|staging\\.example\\.com$/`\n * would allow `https://app.attacker.net`. The source is wrapped as\n * `^(?:source)$`, so every alternative must match the whole origin.\n */\nfunction anchoredPattern(pattern: RegExp, owner: string): RegExp {\n const { source } = pattern;\n const anchored = source.startsWith(\"^\") && source.endsWith(\"$\") && !source.endsWith(\"\\\\$\");\n if (!anchored || pattern.global || pattern.sticky) {\n throw new TypeError(\n `${owner}: an allowedOrigins pattern must match whole origins, anchored with ^ and $ and without the g or y flag; got ${String(pattern)}`,\n );\n }\n return new RegExp(`^(?:${source})$`, pattern.flags);\n}\n\n/**\n * Checks and normalizes an allowlist; throws a `TypeError` naming `owner`\n * for an entry that is neither an origin nor an anchored pattern, or for an\n * empty list unless `allowEmpty`.\n */\nexport function originAllowlist(\n allowedOrigins: readonly AllowedOrigin[],\n owner: string,\n allowEmpty = false,\n): OriginAllowlist {\n if (!Array.isArray(allowedOrigins) || (allowedOrigins.length === 0 && !allowEmpty)) {\n throw new TypeError(`${owner}: allowedOrigins must list the web app's origins`);\n }\n const exact: string[] = [];\n const patterns: RegExp[] = [];\n for (const entry of allowedOrigins) {\n if (entry instanceof RegExp) {\n patterns.push(anchoredPattern(entry, owner));\n } else {\n exact.push(exactOrigin(entry, owner));\n }\n }\n const options: ValidateOriginOptions = {\n // An empty client URL, not undefined: undefined falls back to process.env.CLIENT_URL.\n clientUrl: \"\",\n extraAllowedOrigins: exact,\n allowedPatterns: patterns,\n allowLocalhostInDev: false,\n };\n return Object.freeze({\n allowed: (value: string | undefined) =>\n typeof value === \"string\" && value !== \"\" ? validateRedirectOrigin(value, options) : null,\n fallback: exact[0],\n });\n}\n\n/**\n * The allowlist each `createAuthRoutes` was given, by the session store it\n * writes to: `requireSession` over the same store applies it when it is given\n * no `allowedOrigins` of its own.\n */\nconst ROUTE_ORIGINS = new WeakMap<object, OriginAllowlist>();\n\n/** Remembers the allowlist of the auth routes that write sessions to `sessions`. */\nexport function rememberRouteOrigins(sessions: object, origins: OriginAllowlist): void {\n ROUTE_ORIGINS.set(sessions, origins);\n}\n\n/** The allowlist of the auth routes that write sessions to `sessions`, if any were made. */\nexport function routeOriginsOf(sessions: object): OriginAllowlist | undefined {\n return ROUTE_ORIGINS.get(sessions);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -63,6 +63,8 @@ function callHandler(req, res, _next, options) {
|
|
|
63
63
|
if (!res.getHeader("Retry-After")) {
|
|
64
64
|
res.setHeader("Retry-After", String(Math.ceil(retryAfterMs / 1e3)));
|
|
65
65
|
}
|
|
66
|
+
res.setHeader("Cache-Control", "no-store");
|
|
67
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
66
68
|
res.status(options.statusCode).json({
|
|
67
69
|
ok: false,
|
|
68
70
|
e: { code: "RATE_LIMITED", message: "Rate limit exceeded", data: { retryAfterMs } }
|
|
@@ -87,4 +89,4 @@ export {
|
|
|
87
89
|
createPublicApiLimiter,
|
|
88
90
|
createCallLimiter
|
|
89
91
|
};
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
92
|
+
//# sourceMappingURL=chunk-NW6U45HX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/express/rateLimit.ts"],"sourcesContent":["/**\n * Express rate-limit presets built on express-rate-limit.\n *\n * Lives in the dedicated `/server/express` subpath so the core `/server`\n * entry point carries no express-rate-limit dependency. All presets are\n * factories (not module-level singletons) so env/config is read at call time.\n *\n * Note: distinct from the Socket.IO `createRateLimiter` exported from\n * `/server` — these protect REST routes (OAuth, webhooks, public APIs).\n */\n\nimport type { NextFunction, Request, Response } from \"express\";\nimport {\n ipKeyGenerator,\n rateLimit,\n type Options,\n type RateLimitRequestHandler,\n} from \"express-rate-limit\";\n\ntype PartialOptions = Partial<Options>;\n\nfunction handler(_req: Request, res: Response, _next: NextFunction, options: Options): void {\n const retryAfterSec = Math.max(1, Math.ceil((options.windowMs ?? 60_000) / 1000));\n if (!res.getHeader(\"Retry-After\")) {\n res.setHeader(\"Retry-After\", String(retryAfterSec));\n }\n res.status(options.statusCode ?? 429).json({ error: \"Rate limit exceeded\" });\n}\n\nconst jsonLimiter: PartialOptions = {\n standardHeaders: true,\n legacyHeaders: false,\n handler,\n};\n\n/** Normalize IPv4-mapped IPv6 addresses (::ffff:1.2.3.4 → 1.2.3.4). */\nexport function stripIp(raw: string | undefined): string {\n const ip = raw ?? \"unknown\";\n return ip.startsWith(\"::ffff:\") ? ip.slice(7) : ip;\n}\n\n/**\n * Key by Bearer token when present, falling back to client IP. Lets\n * token-authenticated clients get their own bucket instead of sharing\n * a NAT'd IP. Nothing here checks the token (the 5.1 audit's\n * TRANSPORT-4): a caller that sends a new made-up token on each request\n * gets a fresh bucket each time, even behind `requireSession`, which signs\n * a request in by its cookie first and never reads the bearer token then.\n * Key a limiter mounted behind `requireSession` by the user it verified\n * instead (`keyGenerator: (req) => sessionOf(req).userId`), and anonymous\n * traffic by IP, as `createPublicApiLimiter` does.\n */\nexport function bearerOrIp(req: Request): string {\n const auth = req.headers.authorization ?? \"\";\n if (auth.startsWith(\"Bearer \")) return auth.slice(7);\n return ipKeyGenerator(stripIp(req.ip));\n}\n\n/**\n * Base factory: JSON 429 body + Retry-After header + standard headers.\n */\nexport function createJsonRateLimiter(options: PartialOptions): RateLimitRequestHandler {\n return rateLimit({ ...jsonLimiter, ...options });\n}\n\n/** OAuth flows: 20 requests / 15 min per IP (status sub-routes excluded). */\nexport function createAuthLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 15 * 60 * 1000,\n max: 20,\n skip: (req) => req.path.endsWith(\"/status\"),\n ...overrides,\n });\n}\n\n/** Auth status polling: 120 requests / 15 min per IP. */\nexport function createAuthStatusLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 15 * 60 * 1000,\n max: 120,\n ...overrides,\n });\n}\n\n/** Inbound webhooks: 300 requests / min per IP. */\nexport function createWebhookLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 300,\n ...overrides,\n });\n}\n\n/** Generic public API: 60 requests / min per IP. */\nexport function createPublicApiLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 60,\n ...overrides,\n });\n}\n\n/**\n * Answers a refused call in the HTTP transport's own reply shape, so a\n * server caller (`createServerCaller`) reports `RATE_LIMITED` with\n * `retryAfterMs` instead of an unreadable reply, with the headers of every\n * `/qd` reply (`no-store`, `nosniff`).\n */\nfunction callHandler(req: Request, res: Response, _next: NextFunction, options: Options): void {\n const info = (req as Request & Partial<Record<string, { resetTime?: Date }>>)[\n options.requestPropertyName\n ];\n const untilReset =\n info?.resetTime === undefined ? options.windowMs : info.resetTime.getTime() - Date.now();\n const retryAfterMs = Math.max(1000, untilReset);\n if (!res.getHeader(\"Retry-After\")) {\n res.setHeader(\"Retry-After\", String(Math.ceil(retryAfterMs / 1000)));\n }\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n res.status(options.statusCode).json({\n ok: false,\n e: { code: \"RATE_LIMITED\", message: \"Rate limit exceeded\", data: { retryAfterMs } },\n });\n}\n\n/**\n * Calls through the HTTP transport (RFC 0003 section 10), mounted with\n * `createServer({ http: { rateLimit: createCallLimiter() } })`: 300 calls per\n * minute per IP, refused with `429 { ok: false, e: { code: \"RATE_LIMITED\",\n * message, data: { retryAfterMs } } }`. Server-side prefetch reaches the API\n * from the web server's address, so key it some other way (`keyGenerator`)\n * or raise `max` when the web server prefetches for many users; never by\n * an unverified bearer token (`bearerOrIp`).\n */\nexport function createCallLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 300,\n handler: callHandler,\n ...overrides,\n });\n}\n"],"mappings":";AAYA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAIP,SAAS,QAAQ,MAAe,KAAe,OAAqB,SAAwB;AAC1F,QAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,YAAY,OAAU,GAAI,CAAC;AAChF,MAAI,CAAC,IAAI,UAAU,aAAa,GAAG;AACjC,QAAI,UAAU,eAAe,OAAO,aAAa,CAAC;AAAA,EACpD;AACA,MAAI,OAAO,QAAQ,cAAc,GAAG,EAAE,KAAK,EAAE,OAAO,sBAAsB,CAAC;AAC7E;AAEA,IAAM,cAA8B;AAAA,EAClC,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf;AACF;AAGO,SAAS,QAAQ,KAAiC;AACvD,QAAM,KAAK,OAAO;AAClB,SAAO,GAAG,WAAW,SAAS,IAAI,GAAG,MAAM,CAAC,IAAI;AAClD;AAaO,SAAS,WAAW,KAAsB;AAC/C,QAAM,OAAO,IAAI,QAAQ,iBAAiB;AAC1C,MAAI,KAAK,WAAW,SAAS,EAAG,QAAO,KAAK,MAAM,CAAC;AACnD,SAAO,eAAe,QAAQ,IAAI,EAAE,CAAC;AACvC;AAKO,SAAS,sBAAsB,SAAkD;AACtF,SAAO,UAAU,EAAE,GAAG,aAAa,GAAG,QAAQ,CAAC;AACjD;AAGO,SAAS,kBAAkB,YAA4B,CAAC,GAA4B;AACzF,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,MAAM,CAAC,QAAQ,IAAI,KAAK,SAAS,SAAS;AAAA,IAC1C,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,wBAAwB,YAA4B,CAAC,GAA4B;AAC/F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,qBAAqB,YAA4B,CAAC,GAA4B;AAC5F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,uBAAuB,YAA4B,CAAC,GAA4B;AAC9F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAQA,SAAS,YAAY,KAAc,KAAe,OAAqB,SAAwB;AAC7F,QAAM,OAAQ,IACZ,QAAQ,mBACV;AACA,QAAM,aACJ,MAAM,cAAc,SAAY,QAAQ,WAAW,KAAK,UAAU,QAAQ,IAAI,KAAK,IAAI;AACzF,QAAM,eAAe,KAAK,IAAI,KAAM,UAAU;AAC9C,MAAI,CAAC,IAAI,UAAU,aAAa,GAAG;AACjC,QAAI,UAAU,eAAe,OAAO,KAAK,KAAK,eAAe,GAAI,CAAC,CAAC;AAAA,EACrE;AACA,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,0BAA0B,SAAS;AACjD,MAAI,OAAO,QAAQ,UAAU,EAAE,KAAK;AAAA,IAClC,IAAI;AAAA,IACJ,GAAG,EAAE,MAAM,gBAAgB,SAAS,uBAAuB,MAAM,EAAE,aAAa,EAAE;AAAA,EACpF,CAAC;AACH;AAWO,SAAS,kBAAkB,YAA4B,CAAC,GAA4B;AACzF,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
|