@fragno-dev/backoffice-api 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +90 -2
  3. package/dist/api.d.ts +35 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +10 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/errors.d.ts +45 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +35 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/openapi.d.ts +46 -0
  12. package/dist/openapi.d.ts.map +1 -0
  13. package/dist/openapi.js +205 -0
  14. package/dist/openapi.js.map +1 -0
  15. package/dist/v0/account.d.ts +171 -0
  16. package/dist/v0/account.d.ts.map +1 -0
  17. package/dist/v0/account.js +78 -0
  18. package/dist/v0/account.js.map +1 -0
  19. package/dist/v0/api.d.ts +3437 -0
  20. package/dist/v0/api.d.ts.map +1 -0
  21. package/dist/v0/api.js +52 -0
  22. package/dist/v0/api.js.map +1 -0
  23. package/dist/v0/app-actions.d.ts +105 -0
  24. package/dist/v0/app-actions.d.ts.map +1 -0
  25. package/dist/v0/app-actions.js +100 -0
  26. package/dist/v0/app-actions.js.map +1 -0
  27. package/dist/v0/apps.d.ts +417 -0
  28. package/dist/v0/apps.d.ts.map +1 -0
  29. package/dist/v0/apps.js +163 -0
  30. package/dist/v0/apps.js.map +1 -0
  31. package/dist/v0/automation.d.ts +730 -0
  32. package/dist/v0/automation.d.ts.map +1 -0
  33. package/dist/v0/automation.js +355 -0
  34. package/dist/v0/automation.js.map +1 -0
  35. package/dist/v0/capabilities.d.ts +97 -0
  36. package/dist/v0/capabilities.d.ts.map +1 -0
  37. package/dist/v0/capabilities.js +41 -0
  38. package/dist/v0/capabilities.js.map +1 -0
  39. package/dist/v0/client.d.ts +39 -0
  40. package/dist/v0/client.d.ts.map +1 -0
  41. package/dist/v0/client.js +61 -0
  42. package/dist/v0/client.js.map +1 -0
  43. package/dist/v0/events.d.ts +789 -0
  44. package/dist/v0/events.d.ts.map +1 -0
  45. package/dist/v0/events.js +153 -0
  46. package/dist/v0/events.js.map +1 -0
  47. package/dist/v0/forms.d.ts +172 -0
  48. package/dist/v0/forms.d.ts.map +1 -0
  49. package/dist/v0/forms.js +88 -0
  50. package/dist/v0/forms.js.map +1 -0
  51. package/dist/v0/hooks.d.ts +61 -0
  52. package/dist/v0/hooks.d.ts.map +1 -0
  53. package/dist/v0/hooks.js +49 -0
  54. package/dist/v0/hooks.js.map +1 -0
  55. package/dist/v0/identity.d.ts +73 -0
  56. package/dist/v0/identity.d.ts.map +1 -0
  57. package/dist/v0/identity.js +46 -0
  58. package/dist/v0/identity.js.map +1 -0
  59. package/dist/v0/integrations.d.ts +847 -0
  60. package/dist/v0/integrations.d.ts.map +1 -0
  61. package/dist/v0/integrations.js +255 -0
  62. package/dist/v0/integrations.js.map +1 -0
  63. package/dist/v0/javascript.d.ts +160 -0
  64. package/dist/v0/javascript.d.ts.map +1 -0
  65. package/dist/v0/javascript.js +123 -0
  66. package/dist/v0/javascript.js.map +1 -0
  67. package/dist/v0/marketplace.d.ts +825 -0
  68. package/dist/v0/marketplace.d.ts.map +1 -0
  69. package/dist/v0/marketplace.js +284 -0
  70. package/dist/v0/marketplace.js.map +1 -0
  71. package/dist/v0/organization.d.ts +182 -0
  72. package/dist/v0/organization.d.ts.map +1 -0
  73. package/dist/v0/organization.js +88 -0
  74. package/dist/v0/organization.js.map +1 -0
  75. package/dist/v0/otp.d.ts +37 -0
  76. package/dist/v0/otp.d.ts.map +1 -0
  77. package/dist/v0/otp.js +29 -0
  78. package/dist/v0/otp.js.map +1 -0
  79. package/dist/v0/pi.d.ts +322 -0
  80. package/dist/v0/pi.d.ts.map +1 -0
  81. package/dist/v0/pi.js +146 -0
  82. package/dist/v0/pi.js.map +1 -0
  83. package/dist/v0/sandbox.d.ts +173 -0
  84. package/dist/v0/sandbox.d.ts.map +1 -0
  85. package/dist/v0/sandbox.js +97 -0
  86. package/dist/v0/sandbox.js.map +1 -0
  87. package/dist/v0/shared/bytes.d.ts +12 -0
  88. package/dist/v0/shared/bytes.d.ts.map +1 -0
  89. package/dist/v0/shared/bytes.js +16 -0
  90. package/dist/v0/shared/bytes.js.map +1 -0
  91. package/dist/v0/shared/datetime.d.ts +10 -0
  92. package/dist/v0/shared/datetime.d.ts.map +1 -0
  93. package/dist/v0/shared/datetime.js +14 -0
  94. package/dist/v0/shared/datetime.js.map +1 -0
  95. package/dist/v0/shared/json.d.ts +9 -0
  96. package/dist/v0/shared/json.d.ts.map +1 -0
  97. package/dist/v0/shared/json.js +14 -0
  98. package/dist/v0/shared/json.js.map +1 -0
  99. package/dist/v0/shared/pagination.d.ts +11 -0
  100. package/dist/v0/shared/pagination.d.ts.map +1 -0
  101. package/dist/v0/shared/pagination.js +11 -0
  102. package/dist/v0/shared/pagination.js.map +1 -0
  103. package/dist/v0/shared/permissions.d.ts +279 -0
  104. package/dist/v0/shared/permissions.d.ts.map +1 -0
  105. package/dist/v0/shared/permissions.js +285 -0
  106. package/dist/v0/shared/permissions.js.map +1 -0
  107. package/dist/v0/shared/pi-agent-name.d.ts +10 -0
  108. package/dist/v0/shared/pi-agent-name.d.ts.map +1 -0
  109. package/dist/v0/shared/pi-agent-name.js +11 -0
  110. package/dist/v0/shared/pi-agent-name.js.map +1 -0
  111. package/dist/v0/shared/scope.d.ts +305 -0
  112. package/dist/v0/shared/scope.d.ts.map +1 -0
  113. package/dist/v0/shared/scope.js +279 -0
  114. package/dist/v0/shared/scope.js.map +1 -0
  115. package/dist/v0/state.d.ts +512 -0
  116. package/dist/v0/state.d.ts.map +1 -0
  117. package/dist/v0/state.js +267 -0
  118. package/dist/v0/state.js.map +1 -0
  119. package/dist/v0/store.d.ts +138 -0
  120. package/dist/v0/store.d.ts.map +1 -0
  121. package/dist/v0/store.js +83 -0
  122. package/dist/v0/store.js.map +1 -0
  123. package/dist/v0/upload.d.ts +568 -0
  124. package/dist/v0/upload.d.ts.map +1 -0
  125. package/dist/v0/upload.js +109 -0
  126. package/dist/v0/upload.js.map +1 -0
  127. package/dist/v0/web.d.ts +47 -0
  128. package/dist/v0/web.d.ts.map +1 -0
  129. package/dist/v0/web.js +31 -0
  130. package/dist/v0/web.js.map +1 -0
  131. package/dist/v0/workflow.d.ts +267 -0
  132. package/dist/v0/workflow.d.ts.map +1 -0
  133. package/dist/v0/workflow.js +116 -0
  134. package/dist/v0/workflow.js.map +1 -0
  135. package/package.json +64 -3
package/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ Copyright 2025 - present "ReJot Nederland B.V.", and individual contributors.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the “Software”), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @fragno-dev/backoffice-api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The contract for the Backoffice HTTP API: its operations, their zod input and output schemas, and an
4
+ OpenAPI description generated from them. Backoffice implements this contract; other applications
5
+ import it to call Backoffice with checked types.
6
+
7
+ ## Versions
8
+
9
+ Each version is a separate module (`@fragno-dev/backoffice-api/v0`) served under `/api/<version>`.
10
+ `v0` is unstable: its operations and schemas may change without a new version.
11
+
12
+ Everything a version's requests and responses contain lives inside that version's directory, so a
13
+ new version starts as a copy and the old one stays frozen:
14
+
15
+ - `v0/<family>` (`@fragno-dev/backoffice-api/v0/organization`, …): one family's operations and the
16
+ records they use.
17
+ - `v0/shared/<module>` (`@fragno-dev/backoffice-api/v0/shared/scope`, …): schemas used across
18
+ families, such as scopes, permissions, timestamps, and page inputs.
19
+
20
+ `api`, `errors`, and `openapi` are version-independent: how any version is defined, how errors are
21
+ reported, and how a version is described as OpenAPI.
22
+
23
+ ## Lazy schemas
24
+
25
+ Every module-level schema except an operation's `z.void()` is a `z.lazy(() => …)`, so importing the
26
+ contract builds no schemas until they are used. Backoffice imports it on every Durable Object
27
+ activation, and a request uses only a few of its schemas. Parse, infer, describe, and convert them
28
+ like any schema. For object- or enum-specific members such as `.extend`, `.shape`, `.pick`, or
29
+ `.options`, use the built schema, `schema._zod.innerType`, which is built once and shared;
30
+ `unwrap()` builds a new copy on every call. Keep a schema lazy when you derive from it at module
31
+ level: `z.lazy(() => schema._zod.innerType.extend({ … }))`. Put a schema's `.meta({ id })` on the
32
+ lazy, not inside it, so references to the schema resolve to that id.
33
+
34
+ ## Calling an operation
35
+
36
+ Every operation is `POST /api/<version>/scopes/<scope>/<operationId>` with a bearer token and the
37
+ operation's input as the JSON body. Operations whose input is `z.void()` take no body. The scope is
38
+ `system`, `user:<userId>`, `org:<orgId>`, `app:<owner>:<appId>`, or `project:<owner>:<projectId>`,
39
+ with each id URI-encoded. An app's `<owner>` is the organization or user scope that installed it; a
40
+ project's may also be an app scope, e.g. `project:app:org:<orgId>:<appId>:<projectId>`. Owning a
41
+ scope does not reach into owned apps: an organization's credential cannot act in its apps'
42
+ workspaces, and an event emitted in an app's workspace only reaches that workspace's routes. An
43
+ operation that is not available to the credential in that scope, such as an organization operation
44
+ in a project scope, answers `404`. Each operation lists the permissions it requires in the request's
45
+ scope (`permissions`, from `v0/shared/permissions`); the OpenAPI document repeats them as the roles
46
+ of the operation's `bearer` security requirement. A credential lacking one answers `403`.
47
+
48
+ Successful calls return `200` with the operation's output, or `204` when the output is `z.void()`.
49
+ Bytes travel as standard base64 strings (`v0/shared/bytes`). This is temporary: dedicated file
50
+ transfer APIs will replace it for large files. Errors return `{ error: { code, message } }`
51
+ (`@fragno-dev/backoffice-api/errors`), where the code determines the status: `invalid_request` 400,
52
+ `authentication_failed` 401, `forbidden` 403, `not_found` 404, `conflict` 409, and
53
+ `operation_failed` 422. `not_found` also covers things the request names, such as an unknown id, and
54
+ `operation_failed` is reserved for failures no other code describes.
55
+
56
+ ## Client
57
+
58
+ ```ts
59
+ import { createBackofficeApiClient } from "@fragno-dev/backoffice-api/v0/client";
60
+
61
+ const backoffice = createBackofficeApiClient({
62
+ origin: "https://backoffice.example",
63
+ accessToken,
64
+ fetch,
65
+ });
66
+ const receipt = await backoffice.call({ kind: "org", orgId }, "events.fire", {
67
+ eventType: "bookkeeping.connection.tested",
68
+ payload: { message: "Hello" },
69
+ });
70
+ ```
71
+
72
+ `call` checks the operation id and input at compile time and parses the output with the operation's
73
+ schema. Error responses reject with `BackofficeApiRequestError`, whose `code` is the error
74
+ envelope's code, or `null` when the response was not an API error (for example a server failure), so
75
+ the outcome is unknown. Network failures reject with `BackofficeApiUnreachableError`, whose outcome
76
+ is also unknown. Anything else, such as a success body that does not match the client's contract, is
77
+ a bug and rejects unchanged.
78
+
79
+ ## OpenAPI
80
+
81
+ ```ts
82
+ import { createOpenApiDocument } from "@fragno-dev/backoffice-api/openapi";
83
+ import { backofficeApiV0 } from "@fragno-dev/backoffice-api/v0";
84
+
85
+ const document = createOpenApiDocument(backofficeApiV0, {
86
+ serverUrl: "https://backoffice.example",
87
+ });
88
+ ```
89
+
90
+ `zod` is a peer dependency: schemas share one zod instance with your application so `.meta({ id })`
91
+ names resolve consistently.
package/dist/api.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ //#region src/api.d.ts
3
+ /** A permission named by its namespace, e.g. `{ namespace: "events", permission: "emit" }`. */
4
+ type BackofficeApiPermission = {
5
+ readonly namespace: string;
6
+ readonly permission: string;
7
+ };
8
+ /**
9
+ * One callable operation. Its id is its key in the API's operation map. The caller names the scope
10
+ * it runs in in the request path; Backoffice decides per request whether the operation is
11
+ * available there for the credential, and requires every listed permission in that scope. Input
12
+ * and output are validated by the server; a `z.void()` input means the request has no body, and a
13
+ * `z.void()` output means the response has none.
14
+ */
15
+ type BackofficeApiOperation<TInput extends z.ZodType = z.ZodType, TOutput extends z.ZodType = z.ZodType> = {
16
+ description: string;
17
+ permissions: readonly BackofficeApiPermission[];
18
+ input: TInput;
19
+ output: TOutput;
20
+ };
21
+ /** The first sentence of an operation's description: a one-line answer to what it does. */
22
+ declare function backofficeApiOperationSummary(operation: BackofficeApiOperation): string;
23
+ /** One version of the API, served under `/api/<version>`. */
24
+ type BackofficeApi<TOperations extends Record<string, BackofficeApiOperation> = Record<string, BackofficeApiOperation>> = {
25
+ version: `v${number}`;
26
+ operations: TOperations;
27
+ };
28
+ /**
29
+ * A server's handler for every operation of an API version. Handlers receive input already
30
+ * validated by the operation and return a value its output schema accepts.
31
+ */
32
+ type BackofficeApiImplementation<TApi extends BackofficeApi, TContext> = { [TId in keyof TApi["operations"]]: (input: z.output<TApi["operations"][TId]["input"]>, context: TContext) => Promise<z.input<TApi["operations"][TId]["output"]>>; };
33
+ //#endregion
34
+ export { BackofficeApi, BackofficeApiImplementation, BackofficeApiOperation, BackofficeApiPermission, backofficeApiOperationSummary };
35
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","names":[],"sources":["../src/api.ts"],"mappings":";;;KAGY;WAAqC;WAA4B;;;;;;;;;KASjE,uBACV,eAAe,EAAE,UAAU,EAAE,SAC7B,gBAAgB,EAAE,UAAU,EAAE;EAE9B;EACA,sBAAsB;EACtB,OAAO;EACP,QAAQ;;;iBAIM,8BAA8B,WAAW;;KAM7C,cACV,oBAAoB,eAAe,0BAA0B,eAE3D;EAGF;EACA,YAAY;;;;;;KAOF,4BAA4B,aAAa,eAAe,eACjE,aAAa,sBACZ,OAAO,EAAE,OAAO,mBAAmB,gBACnC,SAAS,aACN,QAAQ,EAAE,MAAM,mBAAmB"}
package/dist/api.js ADDED
@@ -0,0 +1,10 @@
1
+ //#region src/api.ts
2
+ /** The first sentence of an operation's description: a one-line answer to what it does. */
3
+ function backofficeApiOperationSummary(operation) {
4
+ const [summary = operation.description] = operation.description.split(/(?<=\.)\s+(?=[A-Z])/u, 1);
5
+ return summary;
6
+ }
7
+ //#endregion
8
+ export { backofficeApiOperationSummary };
9
+
10
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","names":[],"sources":["../src/api.ts"],"sourcesContent":["import type { z } from \"zod\";\n\n/** A permission named by its namespace, e.g. `{ namespace: \"events\", permission: \"emit\" }`. */\nexport type BackofficeApiPermission = { readonly namespace: string; readonly permission: string };\n\n/**\n * One callable operation. Its id is its key in the API's operation map. The caller names the scope\n * it runs in in the request path; Backoffice decides per request whether the operation is\n * available there for the credential, and requires every listed permission in that scope. Input\n * and output are validated by the server; a `z.void()` input means the request has no body, and a\n * `z.void()` output means the response has none.\n */\nexport type BackofficeApiOperation<\n TInput extends z.ZodType = z.ZodType,\n TOutput extends z.ZodType = z.ZodType,\n> = {\n description: string;\n permissions: readonly BackofficeApiPermission[];\n input: TInput;\n output: TOutput;\n};\n\n/** The first sentence of an operation's description: a one-line answer to what it does. */\nexport function backofficeApiOperationSummary(operation: BackofficeApiOperation): string {\n const [summary = operation.description] = operation.description.split(/(?<=\\.)\\s+(?=[A-Z])/u, 1);\n return summary;\n}\n\n/** One version of the API, served under `/api/<version>`. */\nexport type BackofficeApi<\n TOperations extends Record<string, BackofficeApiOperation> = Record<\n string,\n BackofficeApiOperation\n >,\n> = {\n version: `v${number}`;\n operations: TOperations;\n};\n\n/**\n * A server's handler for every operation of an API version. Handlers receive input already\n * validated by the operation and return a value its output schema accepts.\n */\nexport type BackofficeApiImplementation<TApi extends BackofficeApi, TContext> = {\n [TId in keyof TApi[\"operations\"]]: (\n input: z.output<TApi[\"operations\"][TId][\"input\"]>,\n context: TContext,\n ) => Promise<z.input<TApi[\"operations\"][TId][\"output\"]>>;\n};\n"],"mappings":";;AAuBA,SAAgB,8BAA8B,WAA2C;CACvF,MAAM,CAAC,UAAU,UAAU,eAAe,UAAU,YAAY,MAAM,wBAAwB,CAAC;CAC/F,OAAO;AACT"}
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ //#region src/errors.d.ts
3
+ declare const backofficeApiErrorCodeSchema: z.ZodLazy<z.ZodEnum<{
4
+ authentication_failed: "authentication_failed";
5
+ invalid_request: "invalid_request";
6
+ forbidden: "forbidden";
7
+ not_found: "not_found";
8
+ conflict: "conflict";
9
+ operation_failed: "operation_failed";
10
+ }>>;
11
+ type BackofficeApiErrorCode = z.output<typeof backofficeApiErrorCodeSchema>;
12
+ /** Every error response carries this body; the status follows from the code alone. */
13
+ declare const backofficeApiErrorSchema: z.ZodLazy<z.ZodObject<{
14
+ error: z.ZodObject<{
15
+ code: z.ZodLazy<z.ZodEnum<{
16
+ authentication_failed: "authentication_failed";
17
+ invalid_request: "invalid_request";
18
+ forbidden: "forbidden";
19
+ not_found: "not_found";
20
+ conflict: "conflict";
21
+ operation_failed: "operation_failed";
22
+ }>>;
23
+ message: z.ZodString;
24
+ }, z.core.$strict>;
25
+ }, z.core.$strict>>;
26
+ type BackofficeApiError = z.output<typeof backofficeApiErrorSchema>;
27
+ declare const BACKOFFICE_API_ERROR_STATUS: {
28
+ readonly invalid_request: 400;
29
+ readonly authentication_failed: 401;
30
+ readonly forbidden: 403;
31
+ readonly not_found: 404;
32
+ readonly conflict: 409;
33
+ readonly operation_failed: 422;
34
+ };
35
+ declare const BACKOFFICE_API_ERROR_DESCRIPTIONS: {
36
+ readonly invalid_request: "The scope or the request body is invalid, including values a domain rule rejects.";
37
+ readonly authentication_failed: "The bearer credential is missing, invalid, or expired.";
38
+ readonly forbidden: "The credential may not perform this operation in this scope.";
39
+ readonly not_found: "The operation or something the request names does not exist, or is not available to the credential in this scope.";
40
+ readonly conflict: "The request conflicts with the current state, e.g. something with that name already exists.";
41
+ readonly operation_failed: "The operation ran and failed for a reason no other code describes.";
42
+ };
43
+ //#endregion
44
+ export { BACKOFFICE_API_ERROR_DESCRIPTIONS, BACKOFFICE_API_ERROR_STATUS, BackofficeApiError, BackofficeApiErrorCode, backofficeApiErrorSchema };
45
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;cAEM,8BAA4B,EAAA,QAAA,EAAA;;;;;;;;KAUtB,yBAAyB,EAAE,cAAc;;cAGxC,0BAAwB,EAAA,QAAA,EAAA;;;;;;;;;;;;;KAUzB,qBAAqB,EAAE,cAAc;cAEpC;;;;;;;;cAUA"}
package/dist/errors.js ADDED
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ //#region src/errors.ts
3
+ const backofficeApiErrorCodeSchema = z.lazy(() => z.enum([
4
+ "invalid_request",
5
+ "authentication_failed",
6
+ "forbidden",
7
+ "not_found",
8
+ "conflict",
9
+ "operation_failed"
10
+ ]));
11
+ /** Every error response carries this body; the status follows from the code alone. */
12
+ const backofficeApiErrorSchema = z.lazy(() => z.strictObject({ error: z.strictObject({
13
+ code: backofficeApiErrorCodeSchema,
14
+ message: z.string()
15
+ }) })).meta({ id: "BackofficeApiError" });
16
+ const BACKOFFICE_API_ERROR_STATUS = {
17
+ invalid_request: 400,
18
+ authentication_failed: 401,
19
+ forbidden: 403,
20
+ not_found: 404,
21
+ conflict: 409,
22
+ operation_failed: 422
23
+ };
24
+ const BACKOFFICE_API_ERROR_DESCRIPTIONS = {
25
+ invalid_request: "The scope or the request body is invalid, including values a domain rule rejects.",
26
+ authentication_failed: "The bearer credential is missing, invalid, or expired.",
27
+ forbidden: "The credential may not perform this operation in this scope.",
28
+ not_found: "The operation or something the request names does not exist, or is not available to the credential in this scope.",
29
+ conflict: "The request conflicts with the current state, e.g. something with that name already exists.",
30
+ operation_failed: "The operation ran and failed for a reason no other code describes."
31
+ };
32
+ //#endregion
33
+ export { BACKOFFICE_API_ERROR_DESCRIPTIONS, BACKOFFICE_API_ERROR_STATUS, backofficeApiErrorSchema };
34
+
35
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { z } from \"zod\";\n\nconst backofficeApiErrorCodeSchema = z.lazy(() =>\n z.enum([\n \"invalid_request\",\n \"authentication_failed\",\n \"forbidden\",\n \"not_found\",\n \"conflict\",\n \"operation_failed\",\n ]),\n);\nexport type BackofficeApiErrorCode = z.output<typeof backofficeApiErrorCodeSchema>;\n\n/** Every error response carries this body; the status follows from the code alone. */\nexport const backofficeApiErrorSchema = z\n .lazy(() =>\n z.strictObject({\n error: z.strictObject({\n code: backofficeApiErrorCodeSchema,\n message: z.string(),\n }),\n }),\n )\n .meta({ id: \"BackofficeApiError\" });\nexport type BackofficeApiError = z.output<typeof backofficeApiErrorSchema>;\n\nexport const BACKOFFICE_API_ERROR_STATUS = {\n invalid_request: 400,\n authentication_failed: 401,\n forbidden: 403,\n not_found: 404,\n conflict: 409,\n // The operation ran and failed for a reason no other code describes.\n operation_failed: 422,\n} as const satisfies Record<BackofficeApiErrorCode, number>;\n\nexport const BACKOFFICE_API_ERROR_DESCRIPTIONS = {\n invalid_request:\n \"The scope or the request body is invalid, including values a domain rule rejects.\",\n authentication_failed: \"The bearer credential is missing, invalid, or expired.\",\n forbidden: \"The credential may not perform this operation in this scope.\",\n not_found:\n \"The operation or something the request names does not exist, or is not available to the credential in this scope.\",\n conflict:\n \"The request conflicts with the current state, e.g. something with that name already exists.\",\n operation_failed: \"The operation ran and failed for a reason no other code describes.\",\n} as const satisfies Record<BackofficeApiErrorCode, string>;\n"],"mappings":";;AAEA,MAAM,+BAA+B,EAAE,WACrC,EAAE,KAAK;CACL;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CACH;;AAIA,MAAa,2BAA2B,EACrC,WACC,EAAE,aAAa,EACb,OAAO,EAAE,aAAa;CACpB,MAAM;CACN,SAAS,EAAE,OAAO;AACpB,CAAC,EACH,CAAC,CACH,CAAC,CACA,KAAK,EAAE,IAAI,qBAAqB,CAAC;AAGpC,MAAa,8BAA8B;CACzC,iBAAiB;CACjB,uBAAuB;CACvB,WAAW;CACX,WAAW;CACX,UAAU;CAEV,kBAAkB;AACpB;AAEA,MAAa,oCAAoC;CAC/C,iBACE;CACF,uBAAuB;CACvB,WAAW;CACX,WACE;CACF,UACE;CACF,kBAAkB;AACpB"}
@@ -0,0 +1,46 @@
1
+ import { BackofficeApi } from "./api.js";
2
+ //#region src/openapi.d.ts
3
+ type JsonSchema = {
4
+ [key: string]: unknown;
5
+ };
6
+ type OpenApiDocument = {
7
+ openapi: "3.1.0";
8
+ info: {
9
+ title: string;
10
+ version: string;
11
+ license: {
12
+ name: string;
13
+ identifier: string;
14
+ };
15
+ };
16
+ servers: {
17
+ url: string;
18
+ }[];
19
+ security: {
20
+ bearer: [];
21
+ }[];
22
+ paths: Record<string, {
23
+ post: JsonSchema;
24
+ }>;
25
+ components: {
26
+ schemas: Record<string, JsonSchema>;
27
+ securitySchemes: {
28
+ bearer: JsonSchema;
29
+ };
30
+ };
31
+ };
32
+ /**
33
+ * Describes an API version as OpenAPI 3.1. Every operation is `POST
34
+ * /api/<version>/scopes/{scope}/<operationId>` with its input as the JSON body. Void inputs take no
35
+ * body and void outputs answer `204`; every error response carries `BackofficeApiError`.
36
+ *
37
+ * Schemas with a zod `.meta({ id })` become shared components. A schema whose request form differs
38
+ * from its response form (defaults, transforms) gets a separate `<id>Input` component, but only
39
+ * when both forms are used.
40
+ */
41
+ declare function createOpenApiDocument(api: BackofficeApi, { serverUrl }: {
42
+ serverUrl: string;
43
+ }): OpenApiDocument;
44
+ //#endregion
45
+ export { OpenApiDocument, createOpenApiDocument };
46
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","names":[],"sources":["../src/openapi.ts"],"mappings":";;KAaK;GAAgB;;KAGT;EACV;EACA;IAAQ;IAAe;IAAiB;MAAW;MAAc;;;EACjE;IAAW;;EACX;IAAY;;EACZ,OAAO;IAAiB,MAAM;;EAC9B;IACE,SAAS,eAAe;IACxB;MAAmB,QAAQ;;;;;;;;;;;;;iBAiCf,sBACd,KAAK,iBACH;EAAe;IAChB"}
@@ -0,0 +1,205 @@
1
+ import { backofficeApiOperationSummary } from "./api.js";
2
+ import { BACKOFFICE_API_ERROR_DESCRIPTIONS, BACKOFFICE_API_ERROR_STATUS, backofficeApiErrorSchema } from "./errors.js";
3
+ import { z } from "zod";
4
+ //#region src/openapi.ts
5
+ const SCOPE_ID_PATTERN = "[^:/]+";
6
+ const ROOT_OWNER_PATTERN = `(?:org|user):${SCOPE_ID_PATTERN}`;
7
+ const APP_SCOPE_PATTERN = `app:${ROOT_OWNER_PATTERN}:${SCOPE_ID_PATTERN}`;
8
+ const SCOPE_PARAMETER = {
9
+ name: "scope",
10
+ in: "path",
11
+ required: true,
12
+ description: "Where the operation runs: `system`, `user:<userId>`, `org:<orgId>`, `app:<owner>:<appId>`, or `project:<owner>:<projectId>`. App and project scopes name their owning scope, e.g. `app:org:<orgId>:<appId>` or `project:app:user:<userId>:<appId>:<projectId>`. Operations that are not available to the credential in this scope answer `404`.",
13
+ schema: {
14
+ type: "string",
15
+ pattern: `^(?:system|${ROOT_OWNER_PATTERN}|${APP_SCOPE_PATTERN}|${`project:(?:${ROOT_OWNER_PATTERN}|${APP_SCOPE_PATTERN}):${SCOPE_ID_PATTERN}`})$`
16
+ }
17
+ };
18
+ const DEFINITION_REF_PREFIX = "#/$defs/";
19
+ /**
20
+ * Describes an API version as OpenAPI 3.1. Every operation is `POST
21
+ * /api/<version>/scopes/{scope}/<operationId>` with its input as the JSON body. Void inputs take no
22
+ * body and void outputs answer `204`; every error response carries `BackofficeApiError`.
23
+ *
24
+ * Schemas with a zod `.meta({ id })` become shared components. A schema whose request form differs
25
+ * from its response form (defaults, transforms) gets a separate `<id>Input` component, but only
26
+ * when both forms are used.
27
+ */
28
+ function createOpenApiDocument(api, { serverUrl }) {
29
+ const definitions = {
30
+ input: /* @__PURE__ */ new Map(),
31
+ output: /* @__PURE__ */ new Map()
32
+ };
33
+ function convert(schema, io, conversionName) {
34
+ const { $defs, $schema: _dialect, ...root } = z.toJSONSchema(schema, {
35
+ io,
36
+ target: "draft-2020-12",
37
+ unrepresentable: "throw",
38
+ override: ({ jsonSchema }) => {
39
+ delete jsonSchema["codemodeInputId"];
40
+ delete jsonSchema["codemodeType"];
41
+ }
42
+ });
43
+ const { root: localizedRoot, definitions: localizedDefinitions } = localizeAnonymousDefinitions(root, $defs ?? {}, conversionName);
44
+ for (const [id, definition] of Object.entries(localizedDefinitions)) {
45
+ const existing = definitions[io].get(id);
46
+ if (existing && JSON.stringify(existing) !== JSON.stringify(simplifyCompositions(definition))) throw new Error(`Two different schemas use the OpenAPI component id '${id}'.`);
47
+ definitions[io].set(id, simplifyCompositions(definition));
48
+ }
49
+ return simplifyCompositions(localizedRoot);
50
+ }
51
+ const operations = Object.entries(api.operations).map(([operationId, operation]) => ({
52
+ operationId,
53
+ operation,
54
+ input: operation.input.def.type === "void" ? null : convert(operation.input, "input", `${operationId}.input`),
55
+ output: operation.output.def.type === "void" ? null : convert(operation.output, "output", `${operationId}.output`)
56
+ }));
57
+ const errorSchema = convert(backofficeApiErrorSchema, "output", "error");
58
+ const inputFormDiffers = /* @__PURE__ */ new Map();
59
+ function hasSeparateInputComponent(id) {
60
+ const known = inputFormDiffers.get(id);
61
+ if (known !== void 0) return known;
62
+ const input = definitions.input.get(id);
63
+ const output = definitions.output.get(id);
64
+ if (!input || !output) return false;
65
+ inputFormDiffers.set(id, false);
66
+ const differs = JSON.stringify(input) !== JSON.stringify(output) || referencedDefinitionIds(input).some(hasSeparateInputComponent);
67
+ inputFormDiffers.set(id, differs);
68
+ return differs;
69
+ }
70
+ function componentName(id, io) {
71
+ return io === "input" && hasSeparateInputComponent(id) ? `${id}Input` : id;
72
+ }
73
+ function toComponentRefs(schema, io) {
74
+ return JSON.parse(JSON.stringify(schema), (key, value) => key === "$ref" && typeof value === "string" && value.startsWith(DEFINITION_REF_PREFIX) ? `#/components/schemas/${componentName(value.slice(8), io)}` : value);
75
+ }
76
+ const schemas = {};
77
+ for (const io of ["output", "input"]) for (const [id, definition] of definitions[io]) schemas[componentName(id, io)] = toComponentRefs(definition, io);
78
+ const errorResponses = Object.fromEntries(Object.entries(BACKOFFICE_API_ERROR_STATUS).map(([code, status]) => [String(status), {
79
+ description: BACKOFFICE_API_ERROR_DESCRIPTIONS[code],
80
+ content: { "application/json": { schema: toComponentRefs(errorSchema, "output") } }
81
+ }]));
82
+ const paths = {};
83
+ for (const { operationId, operation, input, output } of operations) paths[`/api/${api.version}/scopes/{scope}/${operationId}`] = { post: {
84
+ operationId,
85
+ ...summarize(operation),
86
+ tags: [operationId.split(".")[0]],
87
+ security: [{ bearer: operation.permissions.map(({ namespace, permission }) => `${namespace}.${permission}`) }],
88
+ parameters: [SCOPE_PARAMETER],
89
+ ...input && { requestBody: {
90
+ required: true,
91
+ content: { "application/json": { schema: toComponentRefs(input, "input") } }
92
+ } },
93
+ responses: {
94
+ ...output ? { "200": {
95
+ description: "The operation's result.",
96
+ content: { "application/json": { schema: toComponentRefs(output, "output") } }
97
+ } } : { "204": { description: "The operation succeeded." } },
98
+ ...errorResponses
99
+ }
100
+ } };
101
+ return {
102
+ openapi: "3.1.0",
103
+ info: {
104
+ title: "Backoffice API",
105
+ version: api.version,
106
+ license: {
107
+ name: "MIT",
108
+ identifier: "MIT"
109
+ }
110
+ },
111
+ servers: [{ url: serverUrl }],
112
+ security: [{ bearer: [] }],
113
+ paths,
114
+ components: {
115
+ schemas,
116
+ securitySchemes: { bearer: {
117
+ type: "http",
118
+ scheme: "bearer",
119
+ bearerFormat: "JWT"
120
+ } }
121
+ }
122
+ };
123
+ }
124
+ /** The description repeats the summary only when there is more to say. */
125
+ function summarize(operation) {
126
+ const summary = backofficeApiOperationSummary(operation);
127
+ return summary === operation.description ? { summary } : {
128
+ summary,
129
+ description: operation.description
130
+ };
131
+ }
132
+ /**
133
+ * zod writes `z.unknown().nullable()` as `anyOf: [{}, { type: "null" }]` and a one-member union as a
134
+ * one-element `oneOf`. Both are valid but illogical: an empty member already matches anything, and
135
+ * a single member is the schema itself.
136
+ */
137
+ function simplifyCompositions(schema) {
138
+ const simplify = (value) => {
139
+ if (Array.isArray(value)) return value.map(simplify);
140
+ if (value === null || typeof value !== "object") return value;
141
+ const node = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, simplify(child)]));
142
+ for (const keyword of ["anyOf", "oneOf"]) {
143
+ const members = node[keyword];
144
+ if (!Array.isArray(members)) continue;
145
+ const { [keyword]: _members, ...rest } = node;
146
+ if (members.some((member) => isEmptySchema(member))) return rest;
147
+ const [only] = members;
148
+ if (members.length === 1 && only && Object.keys(only).every((key) => !(key in rest))) return {
149
+ ...rest,
150
+ ...only
151
+ };
152
+ }
153
+ return node;
154
+ };
155
+ return simplify(schema);
156
+ }
157
+ function isEmptySchema(schema) {
158
+ return schema !== null && typeof schema === "object" && !Array.isArray(schema) && Object.keys(schema).length === 0;
159
+ }
160
+ function referencedDefinitionIds(schema) {
161
+ const ids = [];
162
+ JSON.stringify(schema, (key, value) => {
163
+ if (key === "$ref" && typeof value === "string" && value.startsWith(DEFINITION_REF_PREFIX)) ids.push(value.slice(8));
164
+ return value;
165
+ });
166
+ return ids;
167
+ }
168
+ const ANONYMOUS_DEFINITION = /^__schema\d+$/;
169
+ /**
170
+ * zod names definitions it cannot identify `__schema<n>`, separately in every conversion, and
171
+ * represents a described or re-annotated copy of a recursive schema as an alias of one. Aliases
172
+ * collapse into their named component; other anonymous definitions get a per-conversion name so
173
+ * conversions cannot collide.
174
+ */
175
+ function localizeAnonymousDefinitions(root, definitions, conversionName) {
176
+ const renames = /* @__PURE__ */ new Map();
177
+ for (const [id, definition] of Object.entries(definitions)) {
178
+ const target = aliasTarget(definition);
179
+ if (!ANONYMOUS_DEFINITION.test(id) && target && ANONYMOUS_DEFINITION.test(target)) renames.set(target, id);
180
+ }
181
+ for (const id of Object.keys(definitions)) if (ANONYMOUS_DEFINITION.test(id) && !renames.has(id)) renames.set(id, `${conversionName}.${id.slice(2)}`);
182
+ const rename = (schema) => JSON.parse(JSON.stringify(schema), (key, value) => {
183
+ if (key !== "$ref" || typeof value !== "string" || !value.startsWith(DEFINITION_REF_PREFIX)) return value;
184
+ const id = value.slice(8);
185
+ return `${DEFINITION_REF_PREFIX}${renames.get(id) ?? id}`;
186
+ });
187
+ const localized = {};
188
+ for (const [id, definition] of Object.entries(definitions)) {
189
+ const target = aliasTarget(definition);
190
+ if (target && renames.get(target) === id) continue;
191
+ localized[renames.get(id) ?? id] = rename(definition);
192
+ }
193
+ return {
194
+ root: rename(root),
195
+ definitions: localized
196
+ };
197
+ }
198
+ function aliasTarget(definition) {
199
+ const ref = definition["$ref"];
200
+ return Object.keys(definition).length === 1 && typeof ref === "string" && ref.startsWith(DEFINITION_REF_PREFIX) ? ref.slice(8) : null;
201
+ }
202
+ //#endregion
203
+ export { createOpenApiDocument };
204
+
205
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","names":[],"sources":["../src/openapi.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport {\n type BackofficeApi,\n type BackofficeApiOperation,\n backofficeApiOperationSummary,\n} from \"./api\";\nimport {\n BACKOFFICE_API_ERROR_DESCRIPTIONS,\n BACKOFFICE_API_ERROR_STATUS,\n backofficeApiErrorSchema,\n} from \"./errors\";\n\ntype JsonSchema = { [key: string]: unknown };\ntype SchemaIo = \"input\" | \"output\";\n\nexport type OpenApiDocument = {\n openapi: \"3.1.0\";\n info: { title: string; version: string; license: { name: string; identifier: string } };\n servers: { url: string }[];\n security: { bearer: [] }[];\n paths: Record<string, { post: JsonSchema }>;\n components: {\n schemas: Record<string, JsonSchema>;\n securitySchemes: { bearer: JsonSchema };\n };\n};\n\n// Scope ids are URI-encoded inside the segment, so `:` only separates components.\nconst SCOPE_ID_PATTERN = \"[^:/]+\";\nconst ROOT_OWNER_PATTERN = `(?:org|user):${SCOPE_ID_PATTERN}`;\nconst APP_SCOPE_PATTERN = `app:${ROOT_OWNER_PATTERN}:${SCOPE_ID_PATTERN}`;\nconst PROJECT_SCOPE_PATTERN = `project:(?:${ROOT_OWNER_PATTERN}|${APP_SCOPE_PATTERN}):${SCOPE_ID_PATTERN}`;\n\nconst SCOPE_PARAMETER = {\n name: \"scope\",\n in: \"path\",\n required: true,\n description:\n \"Where the operation runs: `system`, `user:<userId>`, `org:<orgId>`, `app:<owner>:<appId>`, or `project:<owner>:<projectId>`. App and project scopes name their owning scope, e.g. `app:org:<orgId>:<appId>` or `project:app:user:<userId>:<appId>:<projectId>`. Operations that are not available to the credential in this scope answer `404`.\",\n schema: {\n type: \"string\",\n pattern: `^(?:system|${ROOT_OWNER_PATTERN}|${APP_SCOPE_PATTERN}|${PROJECT_SCOPE_PATTERN})$`,\n },\n};\n\nconst DEFINITION_REF_PREFIX = \"#/$defs/\";\n\n/**\n * Describes an API version as OpenAPI 3.1. Every operation is `POST\n * /api/<version>/scopes/{scope}/<operationId>` with its input as the JSON body. Void inputs take no\n * body and void outputs answer `204`; every error response carries `BackofficeApiError`.\n *\n * Schemas with a zod `.meta({ id })` become shared components. A schema whose request form differs\n * from its response form (defaults, transforms) gets a separate `<id>Input` component, but only\n * when both forms are used.\n */\nexport function createOpenApiDocument(\n api: BackofficeApi,\n { serverUrl }: { serverUrl: string },\n): OpenApiDocument {\n const definitions: Record<SchemaIo, Map<string, JsonSchema>> = {\n input: new Map(),\n output: new Map(),\n };\n\n function convert(schema: z.ZodType, io: SchemaIo, conversionName: string): JsonSchema {\n const {\n $defs,\n $schema: _dialect,\n ...root\n } = z.toJSONSchema(schema, {\n io,\n target: \"draft-2020-12\",\n unrepresentable: \"throw\",\n // zod copies every `.meta()` field; Codemode's type-name hints are not JSON Schema.\n override: ({ jsonSchema }) => {\n delete jsonSchema[\"codemodeInputId\"];\n delete jsonSchema[\"codemodeType\"];\n },\n });\n const { root: localizedRoot, definitions: localizedDefinitions } = localizeAnonymousDefinitions(\n root,\n ($defs ?? {}) as Record<string, JsonSchema>,\n conversionName,\n );\n for (const [id, definition] of Object.entries(localizedDefinitions)) {\n const existing = definitions[io].get(id);\n if (\n existing &&\n JSON.stringify(existing) !== JSON.stringify(simplifyCompositions(definition))\n ) {\n throw new Error(`Two different schemas use the OpenAPI component id '${id}'.`);\n }\n definitions[io].set(id, simplifyCompositions(definition));\n }\n return simplifyCompositions(localizedRoot);\n }\n\n const operations = Object.entries(api.operations).map(([operationId, operation]) => ({\n operationId,\n operation,\n // A void input is an operation without a request body, not a JSON value.\n input:\n operation.input.def.type === \"void\"\n ? null\n : convert(operation.input, \"input\", `${operationId}.input`),\n // A void output is a response without a body.\n output:\n operation.output.def.type === \"void\"\n ? null\n : convert(operation.output, \"output\", `${operationId}.output`),\n }));\n const errorSchema = convert(backofficeApiErrorSchema, \"output\", \"error\");\n\n const inputFormDiffers = new Map<string, boolean>();\n function hasSeparateInputComponent(id: string): boolean {\n const known = inputFormDiffers.get(id);\n if (known !== undefined) {\n return known;\n }\n const input = definitions.input.get(id);\n const output = definitions.output.get(id);\n if (!input || !output) {\n return false;\n }\n // Assume equal while visiting so recursive schemas terminate.\n inputFormDiffers.set(id, false);\n const differs =\n JSON.stringify(input) !== JSON.stringify(output) ||\n referencedDefinitionIds(input).some(hasSeparateInputComponent);\n inputFormDiffers.set(id, differs);\n return differs;\n }\n\n function componentName(id: string, io: SchemaIo): string {\n return io === \"input\" && hasSeparateInputComponent(id) ? `${id}Input` : id;\n }\n\n function toComponentRefs(schema: JsonSchema, io: SchemaIo): JsonSchema {\n return JSON.parse(JSON.stringify(schema), (key, value: unknown) =>\n key === \"$ref\" && typeof value === \"string\" && value.startsWith(DEFINITION_REF_PREFIX)\n ? `#/components/schemas/${componentName(value.slice(DEFINITION_REF_PREFIX.length), io)}`\n : value,\n ) as JsonSchema;\n }\n\n const schemas: Record<string, JsonSchema> = {};\n for (const io of [\"output\", \"input\"] as const) {\n for (const [id, definition] of definitions[io]) {\n schemas[componentName(id, io)] = toComponentRefs(definition, io);\n }\n }\n\n const errorResponses = Object.fromEntries(\n Object.entries(BACKOFFICE_API_ERROR_STATUS).map(([code, status]) => [\n String(status),\n {\n description:\n BACKOFFICE_API_ERROR_DESCRIPTIONS[code as keyof typeof BACKOFFICE_API_ERROR_STATUS],\n content: { \"application/json\": { schema: toComponentRefs(errorSchema, \"output\") } },\n },\n ]),\n );\n\n const paths: OpenApiDocument[\"paths\"] = {};\n for (const { operationId, operation, input, output } of operations) {\n paths[`/api/${api.version}/scopes/{scope}/${operationId}`] = {\n post: {\n operationId,\n ...summarize(operation),\n tags: [operationId.split(\".\")[0]],\n // OpenAPI 3.1 lets non-OAuth schemes list the roles a requirement needs.\n security: [\n {\n bearer: operation.permissions.map(\n ({ namespace, permission }) => `${namespace}.${permission}`,\n ),\n },\n ],\n parameters: [SCOPE_PARAMETER],\n ...(input && {\n requestBody: {\n required: true,\n content: { \"application/json\": { schema: toComponentRefs(input, \"input\") } },\n },\n }),\n responses: {\n ...(output\n ? {\n \"200\": {\n description: \"The operation's result.\",\n content: { \"application/json\": { schema: toComponentRefs(output, \"output\") } },\n },\n }\n : { \"204\": { description: \"The operation succeeded.\" } }),\n ...errorResponses,\n },\n },\n };\n }\n\n return {\n openapi: \"3.1.0\",\n // The repository, Backoffice included, is MIT licensed.\n info: {\n title: \"Backoffice API\",\n version: api.version,\n license: { name: \"MIT\", identifier: \"MIT\" },\n },\n servers: [{ url: serverUrl }],\n security: [{ bearer: [] }],\n paths,\n components: {\n schemas,\n securitySchemes: { bearer: { type: \"http\", scheme: \"bearer\", bearerFormat: \"JWT\" } },\n },\n };\n}\n\n/** The description repeats the summary only when there is more to say. */\nfunction summarize(\n operation: BackofficeApiOperation,\n): { summary: string } | { summary: string; description: string } {\n const summary = backofficeApiOperationSummary(operation);\n return summary === operation.description\n ? { summary }\n : { summary, description: operation.description };\n}\n\n/**\n * zod writes `z.unknown().nullable()` as `anyOf: [{}, { type: \"null\" }]` and a one-member union as a\n * one-element `oneOf`. Both are valid but illogical: an empty member already matches anything, and\n * a single member is the schema itself.\n */\nfunction simplifyCompositions(schema: JsonSchema): JsonSchema {\n const simplify = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(simplify);\n }\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n const node = Object.fromEntries(\n Object.entries(value).map(([key, child]) => [key, simplify(child)]),\n ) as JsonSchema;\n for (const keyword of [\"anyOf\", \"oneOf\"] as const) {\n const members = node[keyword];\n if (!Array.isArray(members)) {\n continue;\n }\n const { [keyword]: _members, ...rest } = node;\n if (members.some((member) => isEmptySchema(member))) {\n return rest;\n }\n const [only] = members as JsonSchema[];\n if (members.length === 1 && only && Object.keys(only).every((key) => !(key in rest))) {\n return { ...rest, ...only };\n }\n }\n return node;\n };\n return simplify(schema) as JsonSchema;\n}\n\nfunction isEmptySchema(schema: unknown): boolean {\n return (\n schema !== null &&\n typeof schema === \"object\" &&\n !Array.isArray(schema) &&\n Object.keys(schema).length === 0\n );\n}\n\nfunction referencedDefinitionIds(schema: JsonSchema): string[] {\n const ids: string[] = [];\n JSON.stringify(schema, (key, value: unknown) => {\n if (key === \"$ref\" && typeof value === \"string\" && value.startsWith(DEFINITION_REF_PREFIX)) {\n ids.push(value.slice(DEFINITION_REF_PREFIX.length));\n }\n return value;\n });\n return ids;\n}\n\nconst ANONYMOUS_DEFINITION = /^__schema\\d+$/;\n\n/**\n * zod names definitions it cannot identify `__schema<n>`, separately in every conversion, and\n * represents a described or re-annotated copy of a recursive schema as an alias of one. Aliases\n * collapse into their named component; other anonymous definitions get a per-conversion name so\n * conversions cannot collide.\n */\nfunction localizeAnonymousDefinitions(\n root: JsonSchema,\n definitions: Record<string, JsonSchema>,\n conversionName: string,\n): { root: JsonSchema; definitions: Record<string, JsonSchema> } {\n const renames = new Map<string, string>();\n for (const [id, definition] of Object.entries(definitions)) {\n const target = aliasTarget(definition);\n if (!ANONYMOUS_DEFINITION.test(id) && target && ANONYMOUS_DEFINITION.test(target)) {\n renames.set(target, id);\n }\n }\n for (const id of Object.keys(definitions)) {\n if (ANONYMOUS_DEFINITION.test(id) && !renames.has(id)) {\n renames.set(id, `${conversionName}.${id.slice(\"__\".length)}`);\n }\n }\n const rename = (schema: JsonSchema): JsonSchema =>\n JSON.parse(JSON.stringify(schema), (key, value: unknown) => {\n if (key !== \"$ref\" || typeof value !== \"string\" || !value.startsWith(DEFINITION_REF_PREFIX)) {\n return value;\n }\n const id = value.slice(DEFINITION_REF_PREFIX.length);\n return `${DEFINITION_REF_PREFIX}${renames.get(id) ?? id}`;\n }) as JsonSchema;\n\n const localized: Record<string, JsonSchema> = {};\n for (const [id, definition] of Object.entries(definitions)) {\n const target = aliasTarget(definition);\n // An alias's body is its anonymous target's, which is stored under the alias's name instead.\n if (target && renames.get(target) === id) {\n continue;\n }\n localized[renames.get(id) ?? id] = rename(definition);\n }\n return { root: rename(root), definitions: localized };\n}\n\nfunction aliasTarget(definition: JsonSchema): string | null {\n const ref = definition[\"$ref\"];\n return Object.keys(definition).length === 1 &&\n typeof ref === \"string\" &&\n ref.startsWith(DEFINITION_REF_PREFIX)\n ? ref.slice(DEFINITION_REF_PREFIX.length)\n : null;\n}\n"],"mappings":"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"}