@aglyn/tenant-data-admin 1.0.0-beta.218 → 1.0.0-beta.219

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 (99) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/api-v1-description.d.ts +112 -0
  3. package/src/lib/server/api-v1-description.js +126 -0
  4. package/src/lib/server/api-v1-description.js.map +1 -0
  5. package/src/lib/server/api-v1-kit.d.ts +156 -0
  6. package/src/lib/server/api-v1-kit.js +191 -0
  7. package/src/lib/server/api-v1-kit.js.map +1 -0
  8. package/src/lib/server/api-v1-resources.d.ts +144 -0
  9. package/src/lib/server/api-v1-resources.js +126 -0
  10. package/src/lib/server/api-v1-resources.js.map +1 -0
  11. package/src/lib/server/consent-group-change.d.ts +2 -1
  12. package/src/lib/server/consent-group-change.js +41 -16
  13. package/src/lib/server/consent-group-change.js.map +1 -1
  14. package/src/lib/server/contact-email-index.d.ts +22 -0
  15. package/src/lib/server/contact-email-index.js +108 -0
  16. package/src/lib/server/contact-email-index.js.map +1 -1
  17. package/src/lib/server/crm-email-activity.d.ts +1 -1
  18. package/src/lib/server/crm-email-activity.js +1 -1
  19. package/src/lib/server/crm-email-activity.js.map +1 -1
  20. package/src/lib/server/crm-inbound-email.d.ts +23 -0
  21. package/src/lib/server/crm-inbound-email.js +1 -16
  22. package/src/lib/server/crm-inbound-email.js.map +1 -1
  23. package/src/lib/server/crm-records.d.ts +9 -1
  24. package/src/lib/server/crm-records.js +19 -2
  25. package/src/lib/server/crm-records.js.map +1 -1
  26. package/src/lib/server/cron-auth.d.ts +44 -0
  27. package/src/lib/server/cron-auth.js +76 -0
  28. package/src/lib/server/cron-auth.js.map +1 -0
  29. package/src/lib/server/dataset-live-pages.js +6 -6
  30. package/src/lib/server/dataset-live-pages.js.map +1 -1
  31. package/src/lib/server/duplicate-resource.d.ts +26 -0
  32. package/src/lib/server/duplicate-resource.js +47 -25
  33. package/src/lib/server/duplicate-resource.js.map +1 -1
  34. package/src/lib/server/dynamic-list-materialize.js +4 -1
  35. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  36. package/src/lib/server/email-suppression.d.ts +11 -0
  37. package/src/lib/server/email-suppression.js +36 -1
  38. package/src/lib/server/email-suppression.js.map +1 -1
  39. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  40. package/src/lib/server/email-unsubscribe-link.js +7 -7
  41. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  42. package/src/lib/server/erase.d.ts +19 -33
  43. package/src/lib/server/erase.js +37 -162
  44. package/src/lib/server/erase.js.map +1 -1
  45. package/src/lib/server/host-email-tokens.js +1 -1
  46. package/src/lib/server/host-email-tokens.js.map +1 -1
  47. package/src/lib/server/host-memberships.js +3 -3
  48. package/src/lib/server/host-memberships.js.map +1 -1
  49. package/src/lib/server/member-email-aliases.d.ts +1 -1
  50. package/src/lib/server/member-email-aliases.js +1 -1
  51. package/src/lib/server/member-email-aliases.js.map +1 -1
  52. package/src/lib/server/new-account-announcement.d.ts +62 -0
  53. package/src/lib/server/new-account-announcement.js +77 -0
  54. package/src/lib/server/new-account-announcement.js.map +1 -0
  55. package/src/lib/server/notifications.d.ts +8 -2
  56. package/src/lib/server/notifications.js +11 -3
  57. package/src/lib/server/notifications.js.map +1 -1
  58. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  59. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +1 -1
  60. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  61. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  62. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  63. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  64. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  65. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  66. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  67. package/src/lib/server/payment-provider.d.ts +173 -0
  68. package/src/lib/server/payment-provider.js +70 -0
  69. package/src/lib/server/payment-provider.js.map +1 -0
  70. package/src/lib/server/personal-data-export.js +1 -1
  71. package/src/lib/server/personal-data-export.js.map +1 -1
  72. package/src/lib/server/rate-limit-store.js +13 -3
  73. package/src/lib/server/rate-limit-store.js.map +1 -1
  74. package/src/lib/server/realm-plugins.js +8 -3
  75. package/src/lib/server/realm-plugins.js.map +1 -1
  76. package/src/lib/server/retained-refusals.d.ts +142 -0
  77. package/src/lib/server/retained-refusals.js +325 -0
  78. package/src/lib/server/retained-refusals.js.map +1 -0
  79. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  80. package/src/lib/server/test-firestore-queries.js +70 -27
  81. package/src/lib/server/test-firestore-queries.js.map +1 -1
  82. package/src/lib/server/upsert-contact.d.ts +46 -1
  83. package/src/lib/server/upsert-contact.js +85 -18
  84. package/src/lib/server/upsert-contact.js.map +1 -1
  85. package/src/tenancy.d.ts +4 -5
  86. package/src/tenancy.js +11 -6
  87. package/src/tenancy.js.map +1 -1
  88. package/src/lib/server/connect-account-status.js.map +0 -1
  89. package/src/lib/server/connect-payout-failure.js.map +0 -1
  90. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  91. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  92. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  93. package/src/lib/server/contact-merge.d.ts +0 -55
  94. package/src/lib/server/contact-merge.js +0 -288
  95. package/src/lib/server/contact-merge.js.map +0 -1
  96. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  97. package/src/lib/server/form-dataset-binding-token.js +0 -107
  98. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  99. package/src/lib/server/stripe-account-mode.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-data-admin",
3
- "version": "1.0.0-beta.218",
3
+ "version": "1.0.0-beta.219",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.218",
29
- "@aglyn/shared-util-email": "1.0.0-beta.218",
30
- "@aglyn/shared-util-fbserver": "1.0.0-beta.218",
31
- "@aglyn/shared-util-first-touch": "1.0.0-beta.218",
32
- "@aglyn/shared-util-http": "1.0.0-beta.218",
33
- "@aglyn/shared-util-tools": "1.0.0-beta.218",
28
+ "@aglyn/aglyn": "1.0.0-beta.219",
29
+ "@aglyn/shared-util-email": "1.0.0-beta.219",
30
+ "@aglyn/shared-util-fbserver": "1.0.0-beta.219",
31
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.219",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.219",
33
+ "@aglyn/shared-util-tools": "1.0.0-beta.219",
34
34
  "@msgpack/msgpack": "^3.1.3",
35
35
  "@swc/helpers": "0.5.23",
36
36
  "sharp": "^0.35.5",
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * How a `/v1` resource is described in the customer API's OpenAPI document
19
+ * (AGL-2733), in the shape the console's builder turns into paths, tags and
20
+ * schemas — whoever serves the resource.
21
+ *
22
+ * The platform describes its own resources with these; a plugin that serves
23
+ * one (`api-v1-resources.ts`) hands the builder its description through the
24
+ * registration, so the document, and the MCP tools derived from it, describe
25
+ * every resource the build serves and nothing it does not.
26
+ *
27
+ * Pure data and pure functions: nothing here reads a request or a store.
28
+ */
29
+ /** A JSON Schema / OpenAPI fragment. */
30
+ export type ApiV1Schema = Record<string, unknown>;
31
+ export interface ApiV1Operation {
32
+ /** Path relative to the server, e.g. `/v1/contacts/{contactId}`. */
33
+ readonly path: string;
34
+ readonly method: 'get' | 'post' | 'patch' | 'delete';
35
+ readonly operationId: string;
36
+ readonly summary: string;
37
+ readonly description?: string;
38
+ /** `true` when the response is the paginated list envelope. */
39
+ readonly list?: boolean;
40
+ /** Schema name for the response body, when it is a single record. */
41
+ readonly returns?: string;
42
+ /** Schema name for the request body. */
43
+ readonly accepts?: string;
44
+ /** Extra query parameters beyond the pagination pair. */
45
+ readonly filters?: readonly ApiV1Schema[];
46
+ /** Path parameters, in order. */
47
+ readonly pathParams?: readonly {
48
+ name: string;
49
+ description: string;
50
+ }[];
51
+ /**
52
+ * `true` for a create: a fresh one answers `201`, and a replay of the same
53
+ * `Idempotency-Key` answers `200` with the record the original create made.
54
+ */
55
+ readonly creates?: boolean;
56
+ /** Entitlement the call needs, named in the 403. */
57
+ readonly entitlement?: string;
58
+ }
59
+ export interface ApiV1ResourceDescription {
60
+ readonly tag: string;
61
+ readonly description: string;
62
+ readonly schemaName: string;
63
+ readonly fields: Record<string, ApiV1Schema>;
64
+ readonly required: readonly string[];
65
+ readonly ops: readonly ApiV1Operation[];
66
+ /**
67
+ * The members a write body may carry: the handler's own writable set, in the
68
+ * order they are described. A record returns members nothing can write —
69
+ * `siteId`, `sources`, the stamps — and a closed body listing them would tell
70
+ * a generated client to send what the handler refuses.
71
+ */
72
+ readonly writable?: readonly string[];
73
+ /** Schemas for writable members the record does not return, or returns differently. */
74
+ readonly writeOnly?: Record<string, ApiV1Schema>;
75
+ /** What a create accepts that an update does not, in words. */
76
+ readonly writeNote?: string;
77
+ /** Members a write must carry. Only for a body a single method takes. */
78
+ readonly writeRequired?: readonly string[];
79
+ /**
80
+ * Further named schemas the operations reference — a request body that is
81
+ * not the record's write shape (`ContactMerge`), a response that is not the
82
+ * record (`LeadConversion`). Each replaces a schema of the same name the
83
+ * builder would otherwise derive.
84
+ */
85
+ readonly components?: Record<string, ApiV1Schema>;
86
+ }
87
+ export declare const isoField: (description: string) => ApiV1Schema;
88
+ export declare const stringField: (description: string) => ApiV1Schema;
89
+ export declare const integerField: (description: string) => ApiV1Schema;
90
+ export declare const booleanField: (description: string) => ApiV1Schema;
91
+ export declare const stringListField: (description: string) => ApiV1Schema;
92
+ /**
93
+ * A field the API may answer with `null`.
94
+ *
95
+ * OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and
96
+ * not the 3.0 `nullable: true` keyword — a generator reading `nullable` here
97
+ * would silently produce a non-optional field, which is the whole class of bug
98
+ * the document exists to remove.
99
+ */
100
+ export declare const nullableField: (base: ApiV1Schema) => ApiV1Schema;
101
+ export declare const openObjectField: (description: string) => ApiV1Schema;
102
+ /** A record's `object` member, which names its kind. */
103
+ export declare const objectKindField: (name: string) => ApiV1Schema;
104
+ export declare const postalAddressField: () => ApiV1Schema;
105
+ /** Every record carries these two. */
106
+ export declare const RECORD_STAMPS: {
107
+ created: ApiV1Schema;
108
+ updated: ApiV1Schema;
109
+ };
110
+ export declare const UPDATED_AFTER_PARAM: ApiV1Schema;
111
+ /** An optional query parameter a list filters by. */
112
+ export declare const queryParam: (name: string, description: string, schema?: ApiV1Schema) => ApiV1Schema;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * How a `/v1` resource is described in the customer API's OpenAPI document
18
+ * (AGL-2733), in the shape the console's builder turns into paths, tags and
19
+ * schemas — whoever serves the resource.
20
+ *
21
+ * The platform describes its own resources with these; a plugin that serves
22
+ * one (`api-v1-resources.ts`) hands the builder its description through the
23
+ * registration, so the document, and the MCP tools derived from it, describe
24
+ * every resource the build serves and nothing it does not.
25
+ *
26
+ * Pure data and pure functions: nothing here reads a request or a store.
27
+ */ /** A JSON Schema / OpenAPI fragment. */ import { _ as _extends } from "@swc/helpers/_/_extends";
28
+ // ── Field schemas ──────────────────────────────────────────────────────────
29
+ export const isoField = (description)=>({
30
+ type: 'string',
31
+ format: 'date-time',
32
+ description
33
+ });
34
+ export const stringField = (description)=>({
35
+ type: 'string',
36
+ description
37
+ });
38
+ export const integerField = (description)=>({
39
+ type: 'integer',
40
+ description
41
+ });
42
+ export const booleanField = (description)=>({
43
+ type: 'boolean',
44
+ description
45
+ });
46
+ export const stringListField = (description)=>({
47
+ type: 'array',
48
+ items: {
49
+ type: 'string'
50
+ },
51
+ description
52
+ });
53
+ /**
54
+ * A field the API may answer with `null`.
55
+ *
56
+ * OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and
57
+ * not the 3.0 `nullable: true` keyword — a generator reading `nullable` here
58
+ * would silently produce a non-optional field, which is the whole class of bug
59
+ * the document exists to remove.
60
+ */ export const nullableField = (base)=>_extends({}, base, {
61
+ type: [
62
+ base['type'],
63
+ 'null'
64
+ ]
65
+ });
66
+ export const openObjectField = (description)=>({
67
+ type: 'object',
68
+ description,
69
+ additionalProperties: true
70
+ });
71
+ /** A record's `object` member, which names its kind. */ export const objectKindField = (name)=>({
72
+ type: 'string',
73
+ const: name,
74
+ description: `Always \`${name}\`. Lets a client discriminate a mixed array.`
75
+ });
76
+ export const postalAddressField = ()=>({
77
+ type: 'object',
78
+ description: 'Postal address. Members are optional and free-form.',
79
+ properties: {
80
+ line1: {
81
+ type: 'string'
82
+ },
83
+ line2: {
84
+ type: 'string'
85
+ },
86
+ city: {
87
+ type: 'string'
88
+ },
89
+ region: {
90
+ type: 'string'
91
+ },
92
+ postalCode: {
93
+ type: 'string'
94
+ },
95
+ country: {
96
+ type: 'string'
97
+ }
98
+ },
99
+ additionalProperties: true
100
+ });
101
+ /** Every record carries these two. */ export const RECORD_STAMPS = {
102
+ created: isoField('When the record was created.'),
103
+ updated: isoField('When the record last changed. CRM lists can be walked by it.')
104
+ };
105
+ // ── Query parameters ───────────────────────────────────────────────────────
106
+ export const UPDATED_AFTER_PARAM = {
107
+ name: 'updatedAfter',
108
+ in: 'query',
109
+ required: false,
110
+ description: 'ISO 8601 instant WITH an offset (`2026-09-01T00:00:00Z`). A bare date ' + 'is a 400 — midnight in whose zone is not a question this API can ' + 'answer. Reorders the list by `updated` ascending, and moves every other ' + 'filter out of the query and onto the page, so pages can come back short.',
111
+ schema: {
112
+ type: 'string',
113
+ format: 'date-time'
114
+ }
115
+ };
116
+ /** An optional query parameter a list filters by. */ export const queryParam = (name, description, schema = {
117
+ type: 'string'
118
+ })=>({
119
+ name,
120
+ in: 'query',
121
+ required: false,
122
+ description,
123
+ schema
124
+ });
125
+
126
+ //# sourceMappingURL=api-v1-description.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/api-v1-description.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How a `/v1` resource is described in the customer API's OpenAPI document\n * (AGL-2733), in the shape the console's builder turns into paths, tags and\n * schemas — whoever serves the resource.\n *\n * The platform describes its own resources with these; a plugin that serves\n * one (`api-v1-resources.ts`) hands the builder its description through the\n * registration, so the document, and the MCP tools derived from it, describe\n * every resource the build serves and nothing it does not.\n *\n * Pure data and pure functions: nothing here reads a request or a store.\n */\n\n/** A JSON Schema / OpenAPI fragment. */\nexport type ApiV1Schema = Record<string, unknown>\n\nexport interface ApiV1Operation {\n /** Path relative to the server, e.g. `/v1/contacts/{contactId}`. */\n readonly path: string\n readonly method: 'get' | 'post' | 'patch' | 'delete'\n readonly operationId: string\n readonly summary: string\n readonly description?: string\n /** `true` when the response is the paginated list envelope. */\n readonly list?: boolean\n /** Schema name for the response body, when it is a single record. */\n readonly returns?: string\n /** Schema name for the request body. */\n readonly accepts?: string\n /** Extra query parameters beyond the pagination pair. */\n readonly filters?: readonly ApiV1Schema[]\n /** Path parameters, in order. */\n readonly pathParams?: readonly { name: string; description: string }[]\n /**\n * `true` for a create: a fresh one answers `201`, and a replay of the same\n * `Idempotency-Key` answers `200` with the record the original create made.\n */\n readonly creates?: boolean\n /** Entitlement the call needs, named in the 403. */\n readonly entitlement?: string\n}\n\nexport interface ApiV1ResourceDescription {\n readonly tag: string\n readonly description: string\n readonly schemaName: string\n readonly fields: Record<string, ApiV1Schema>\n readonly required: readonly string[]\n readonly ops: readonly ApiV1Operation[]\n /**\n * The members a write body may carry: the handler's own writable set, in the\n * order they are described. A record returns members nothing can write —\n * `siteId`, `sources`, the stamps — and a closed body listing them would tell\n * a generated client to send what the handler refuses.\n */\n readonly writable?: readonly string[]\n /** Schemas for writable members the record does not return, or returns differently. */\n readonly writeOnly?: Record<string, ApiV1Schema>\n /** What a create accepts that an update does not, in words. */\n readonly writeNote?: string\n /** Members a write must carry. Only for a body a single method takes. */\n readonly writeRequired?: readonly string[]\n /**\n * Further named schemas the operations reference — a request body that is\n * not the record's write shape (`ContactMerge`), a response that is not the\n * record (`LeadConversion`). Each replaces a schema of the same name the\n * builder would otherwise derive.\n */\n readonly components?: Record<string, ApiV1Schema>\n}\n\n// ── Field schemas ──────────────────────────────────────────────────────────\n\nexport const isoField = (description: string): ApiV1Schema => ({\n type: 'string',\n format: 'date-time',\n description,\n})\n\nexport const stringField = (description: string): ApiV1Schema => ({ type: 'string', description })\nexport const integerField = (description: string): ApiV1Schema => ({ type: 'integer', description })\nexport const booleanField = (description: string): ApiV1Schema => ({ type: 'boolean', description })\nexport const stringListField = (description: string): ApiV1Schema => ({\n type: 'array',\n items: { type: 'string' },\n description,\n})\n\n/**\n * A field the API may answer with `null`.\n *\n * OpenAPI 3.1 is JSON Schema 2020-12, where nullability is a type UNION and\n * not the 3.0 `nullable: true` keyword — a generator reading `nullable` here\n * would silently produce a non-optional field, which is the whole class of bug\n * the document exists to remove.\n */\nexport const nullableField = (base: ApiV1Schema): ApiV1Schema => ({\n ...base,\n type: [base['type'], 'null'],\n})\n\nexport const openObjectField = (description: string): ApiV1Schema => ({\n type: 'object',\n description,\n additionalProperties: true,\n})\n\n/** A record's `object` member, which names its kind. */\nexport const objectKindField = (name: string): ApiV1Schema => ({\n type: 'string',\n const: name,\n description: `Always \\`${name}\\`. Lets a client discriminate a mixed array.`,\n})\n\nexport const postalAddressField = (): ApiV1Schema => ({\n type: 'object',\n description: 'Postal address. Members are optional and free-form.',\n properties: {\n line1: { type: 'string' },\n line2: { type: 'string' },\n city: { type: 'string' },\n region: { type: 'string' },\n postalCode: { type: 'string' },\n country: { type: 'string' },\n },\n additionalProperties: true,\n})\n\n/** Every record carries these two. */\nexport const RECORD_STAMPS = {\n created: isoField('When the record was created.'),\n updated: isoField('When the record last changed. CRM lists can be walked by it.'),\n}\n\n// ── Query parameters ───────────────────────────────────────────────────────\n\nexport const UPDATED_AFTER_PARAM: ApiV1Schema = {\n name: 'updatedAfter',\n in: 'query',\n required: false,\n description:\n 'ISO 8601 instant WITH an offset (`2026-09-01T00:00:00Z`). A bare date ' +\n 'is a 400 — midnight in whose zone is not a question this API can ' +\n 'answer. Reorders the list by `updated` ascending, and moves every other ' +\n 'filter out of the query and onto the page, so pages can come back short.',\n schema: { type: 'string', format: 'date-time' },\n}\n\n/** An optional query parameter a list filters by. */\nexport const queryParam = (\n name: string,\n description: string,\n schema: ApiV1Schema = { type: 'string' },\n): ApiV1Schema => ({\n name,\n in: 'query',\n required: false,\n description,\n schema,\n})\n"],"names":["isoField","description","type","format","stringField","integerField","booleanField","stringListField","items","nullableField","base","openObjectField","additionalProperties","objectKindField","name","const","postalAddressField","properties","line1","line2","city","region","postalCode","country","RECORD_STAMPS","created","updated","UPDATED_AFTER_PARAM","in","required","schema","queryParam"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,sCAAsC;AA0DtC,8EAA8E;AAE9E,OAAO,MAAMA,WAAW,CAACC,cAAsC,CAAA;QAC7DC,MAAM;QACNC,QAAQ;QACRF;IACF,CAAA,EAAE;AAEF,OAAO,MAAMG,cAAc,CAACH,cAAsC,CAAA;QAAEC,MAAM;QAAUD;IAAY,CAAA,EAAE;AAClG,OAAO,MAAMI,eAAe,CAACJ,cAAsC,CAAA;QAAEC,MAAM;QAAWD;IAAY,CAAA,EAAE;AACpG,OAAO,MAAMK,eAAe,CAACL,cAAsC,CAAA;QAAEC,MAAM;QAAWD;IAAY,CAAA,EAAE;AACpG,OAAO,MAAMM,kBAAkB,CAACN,cAAsC,CAAA;QACpEC,MAAM;QACNM,OAAO;YAAEN,MAAM;QAAS;QACxBD;IACF,CAAA,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMQ,gBAAgB,CAACC,OAAoC,aAC7DA;QACHR,MAAM;YAACQ,IAAI,CAAC,OAAO;YAAE;SAAO;OAC5B;AAEF,OAAO,MAAMC,kBAAkB,CAACV,cAAsC,CAAA;QACpEC,MAAM;QACND;QACAW,sBAAsB;IACxB,CAAA,EAAE;AAEF,sDAAsD,GACtD,OAAO,MAAMC,kBAAkB,CAACC,OAA+B,CAAA;QAC7DZ,MAAM;QACNa,OAAOD;QACPb,aAAa,CAAC,SAAS,EAAEa,KAAK,6CAA6C,CAAC;IAC9E,CAAA,EAAE;AAEF,OAAO,MAAME,qBAAqB,IAAoB,CAAA;QACpDd,MAAM;QACND,aAAa;QACbgB,YAAY;YACVC,OAAO;gBAAEhB,MAAM;YAAS;YACxBiB,OAAO;gBAAEjB,MAAM;YAAS;YACxBkB,MAAM;gBAAElB,MAAM;YAAS;YACvBmB,QAAQ;gBAAEnB,MAAM;YAAS;YACzBoB,YAAY;gBAAEpB,MAAM;YAAS;YAC7BqB,SAAS;gBAAErB,MAAM;YAAS;QAC5B;QACAU,sBAAsB;IACxB,CAAA,EAAE;AAEF,oCAAoC,GACpC,OAAO,MAAMY,gBAAgB;IAC3BC,SAASzB,SAAS;IAClB0B,SAAS1B,SAAS;AACpB,EAAC;AAED,8EAA8E;AAE9E,OAAO,MAAM2B,sBAAmC;IAC9Cb,MAAM;IACNc,IAAI;IACJC,UAAU;IACV5B,aACE,2EACA,sEACA,6EACA;IACF6B,QAAQ;QAAE5B,MAAM;QAAUC,QAAQ;IAAY;AAChD,EAAC;AAED,mDAAmD,GACnD,OAAO,MAAM4B,aAAa,CACxBjB,MACAb,aACA6B,SAAsB;IAAE5B,MAAM;AAAS,CAAC,GACvB,CAAA;QACjBY;QACAc,IAAI;QACJC,UAAU;QACV5B;QACA6B;IACF,CAAA,EAAE"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * What every `/v1` resource handler shares (AGL-2606): the context the
19
+ * pipeline hands it, the scope check, the cursor, serialization and
20
+ * idempotency helpers, and the shape a usage figure is published in.
21
+ *
22
+ * One copy for every resource, whoever serves it. A second copy of the
23
+ * cursor grammar or the idempotency claim in any handler is exactly the drift
24
+ * these exist to prevent: a cursor one resource encodes and another cannot
25
+ * decode, or a claim digest that collides across resources because one copy
26
+ * forgot the org. They are the data layer's because a resource a plugin
27
+ * serves (`api-v1-resources.ts`) is a handler too, and a plugin may not
28
+ * import the console.
29
+ */
30
+ import { type AttemptClaim } from '@aglyn/aglyn/server';
31
+ import type { AglynOrganization } from '@aglyn/aglyn';
32
+ import type { ApiScope } from './api-keys';
33
+ /** What the pipeline resolved for one authenticated request. */
34
+ export interface ApiV1Context {
35
+ orgId: string;
36
+ keyId: string;
37
+ /**
38
+ * The key's name, as the organization labeled it, or null for a key with
39
+ * none. What a write made with the key is attributed to in the console's
40
+ * activity feeds (AGL-2632) — the key is the actor, and "API key Zapier"
41
+ * is its name the way an address is a person's.
42
+ */
43
+ keyName: string | null;
44
+ scopes: ApiScope[];
45
+ org: Partial<AglynOrganization>;
46
+ firestore: FirebaseFirestore.Firestore;
47
+ /** Rate-limit headers to echo on every response for this request. */
48
+ headers: Record<string, string>;
49
+ }
50
+ /** Return a 403 unless the key carries `scope`; otherwise `null` (proceed). */
51
+ export declare function requireScope(context: ApiV1Context, scope: ApiScope): Response | null;
52
+ /** Firestore values → JSON-safe values (Timestamps become ISO strings). */
53
+ export declare function serialize(value: unknown): unknown;
54
+ export interface Paginated {
55
+ docs: FirebaseFirestore.QueryDocumentSnapshot[];
56
+ nextCursor: string | null;
57
+ }
58
+ export declare function paginate(query: FirebaseFirestore.Query, url: URL): Promise<Paginated>;
59
+ export declare function readJsonBody(request: Request): Promise<Record<string, unknown>>;
60
+ /**
61
+ * The operations a claim can belong to. Hashed into the digest, so a key
62
+ * reused across two of them is two separate attempts — see `claimWrite`.
63
+ *
64
+ * The CRM pairs (AGL-2606) follow the contact pair's shape: a create and a
65
+ * delete per resource, sharing the organization as their scope suffix and
66
+ * kept apart by the kind alone.
67
+ */
68
+ export type ClaimKind = 'records' | 'record-deletes' | 'datasets' | 'dataset-deletes' | 'form-submission-deletes' | 'contacts' | 'contact-deletes' | 'contact-merges' | 'media' | 'sites' | 'companies' | 'company-deletes' | 'deals' | 'deal-deletes' | 'tasks' | 'task-deletes' | 'activities' | 'activity-deletes' | 'lead-conversions' | 'lead-creates' | 'email-templates' | 'email-template-deletes';
69
+ /**
70
+ * Take an exclusive claim on one write attempt (AGL-1709), and translate the
71
+ * transport-agnostic result into a v1 `Response`.
72
+ *
73
+ * The claim itself is the shared `claimAttempt` (AGL-1697, `220217133`) — an
74
+ * unconditional `create()`, where Firestore's rejection on an existing document
75
+ * IS the dedupe primitive. This resource used to run its own read-then-create,
76
+ * which is the exact race the mechanism exists to prevent: two concurrent
77
+ * requests carrying one key both read "no prior key", both fall through, both
78
+ * create. Worse than no check at all in one respect, because it made the path
79
+ * look protected. Adopting the shared claim rather than fixing the local copy
80
+ * is the point — POS, refunds and the REST API now answer this question once.
81
+ *
82
+ * `scopeId` carries the ORG as well as the dataset. The helper hashes
83
+ * `{kind}:{scopeId}:{key}` and its plugin callers pass a globally unique host
84
+ * id, but a dataset id is only meaningful under its org — and the claim stores
85
+ * the response body, so a cross-org digest collision would replay one tenant's
86
+ * record to another. The org belongs in the digest, not just in the swept
87
+ * field. Scoping by the DATASET is what `apps/docs/api/conventions.md` already
88
+ * publishes ("replay is looked up within the dataset you're posting to"); the
89
+ * old org-only digest only reached that outcome by accident, by reading a
90
+ * `recordId` that did not exist in the other dataset and falling through — and
91
+ * having fallen through, its key write failed and was swallowed, so the key
92
+ * never deduped there at all.
93
+ *
94
+ * NOTE: the digest changes, so keys stored under the old `orgId:key` shape are
95
+ * orphaned and a retry straddling this deploy creates a duplicate. Accepted —
96
+ * the API is pre-beta and the alternative is carrying the race forever.
97
+ *
98
+ * `kind` separates the create's claims from the delete's (AGL-1710). The
99
+ * helper hashes it into the digest for exactly this reason, and the two record
100
+ * different response BODIES — a record view versus a `{ deleted: true }`
101
+ * receipt. Sharing one namespace would let a key reused across both operations
102
+ * replay the create's record to a delete, which a client parses as a success
103
+ * and which no amount of documentation makes safe. `POST` keeps `records`
104
+ * verbatim: changing its digest orphans keys in flight, and AGL-1709 already
105
+ * paid that cost once.
106
+ *
107
+ * `scopeSuffix` is the object the key is scoped WITHIN — a dataset id for the
108
+ * record operations and for `DELETE /v1/datasets/{id}`, the site for
109
+ * `DELETE …/form-submissions/{id}`, and `*` — the ORGANIZATION — for
110
+ * `POST /v1/datasets` (AGL-2126), where there is no dataset id yet because
111
+ * the request is what creates one, and for both contact writes (AGL-2276),
112
+ * where the organization genuinely IS the containing object: contacts are
113
+ * org-scoped (AGL-237), so there is no site or parent collection to scope to
114
+ * and pretending otherwise would invent a boundary the data does not have.
115
+ * `*` cannot collide with a resource id: `createResourceUid()` never emits
116
+ * one, and the `kind` is separate anyway. The suffix is not the whole story —
117
+ * `kind` is hashed in too, which is what keeps a create's key from replaying
118
+ * into a delete, and what keeps `contacts` and `contact-deletes` apart while
119
+ * they share a suffix.
120
+ *
121
+ * This paragraph used to say `*` covered "the two that act on the dataset
122
+ * collection itself", which the delete has never done — it passes
123
+ * `datasetRef.id`. The published contract in `apps/docs/api/conventions.md` was
124
+ * written from this comment and inherited the error (AGL-2218), which is the
125
+ * argument for reading the call rather than the docblock above it.
126
+ */
127
+ export declare function claimWrite(ctx: ApiV1Context, scopeSuffix: string, key: string | null, kind: ClaimKind): Promise<{
128
+ claim: AttemptClaim;
129
+ } | {
130
+ replay: Response;
131
+ }>;
132
+ /**
133
+ * One metered dimension, as published (AGL-2277).
134
+ *
135
+ * `included`/`remaining` are `null` for an UNLIMITED band rather than the
136
+ * sentinel itself: `UNLIMITED` is `Number.POSITIVE_INFINITY`, which
137
+ * `JSON.stringify` silently turns into `null` anyway — so the choice is
138
+ * between a `null` that means "unlimited" on purpose and the same `null`
139
+ * arriving by accident, indistinguishable from a bug. Stated explicitly, and
140
+ * documented, so an integrator can branch on it.
141
+ *
142
+ * `metered` is the field that actually answers "what happens when I cross
143
+ * this" — true means the excess bills, false means the next call is refused.
144
+ * It is read off the plan's own overage rate through the `check*Quota`
145
+ * helpers rather than re-derived here, because a second copy of that rule
146
+ * would drift from the one the enforcement path uses, and this endpoint's
147
+ * whole value is telling a customer what the enforcement path will do.
148
+ */
149
+ export declare function usageBand(used: number, included: number, remaining: number, overageRateUsd: number | null): {
150
+ used: number;
151
+ included: number;
152
+ remaining: number;
153
+ metered: boolean;
154
+ };
155
+ /** Whether `hostId` is one of the authenticated organization's own sites. */
156
+ export declare function orgOwnsHost(ctx: ApiV1Context, hostId: string): boolean;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * What every `/v1` resource handler shares (AGL-2606): the context the
18
+ * pipeline hands it, the scope check, the cursor, serialization and
19
+ * idempotency helpers, and the shape a usage figure is published in.
20
+ *
21
+ * One copy for every resource, whoever serves it. A second copy of the
22
+ * cursor grammar or the idempotency claim in any handler is exactly the drift
23
+ * these exist to prevent: a cursor one resource encodes and another cannot
24
+ * decode, or a claim digest that collides across resources because one copy
25
+ * forgot the org. They are the data layer's because a resource a plugin
26
+ * serves (`api-v1-resources.ts`) is a handler too, and a plugin may not
27
+ * import the console.
28
+ */ import { claimAttempt } from "@aglyn/aglyn/server";
29
+ import { FieldPath, Timestamp } from "firebase-admin/firestore";
30
+ import { apiJson, ApiErrors, decodeCursor, encodeCursor, parseLimit } from "./api-http.js";
31
+ /** Return a 403 unless the key carries `scope`; otherwise `null` (proceed). */ export function requireScope(context, scope) {
32
+ return context.scopes.includes(scope) ? null : ApiErrors.insufficientScope(scope, context.headers);
33
+ }
34
+ // ── Serialization ───────────────────────────────────────────────────────────
35
+ /** Firestore values → JSON-safe values (Timestamps become ISO strings). */ export function serialize(value) {
36
+ if (value instanceof Timestamp) return value.toDate().toISOString();
37
+ if (Array.isArray(value)) return value.map(serialize);
38
+ if (value && typeof value === 'object') {
39
+ const out = {};
40
+ for (const [k, v] of Object.entries(value))out[k] = serialize(v);
41
+ return out;
42
+ }
43
+ return value;
44
+ }
45
+ export async function paginate(query, url) {
46
+ const limit = parseLimit(url.searchParams.get('limit'));
47
+ const cursor = decodeCursor(url.searchParams.get('cursor'));
48
+ let q = query.orderBy(FieldPath.documentId()).limit(limit + 1);
49
+ if (cursor) q = q.startAfter(cursor);
50
+ const snap = await q.get();
51
+ const docs = snap.docs.slice(0, limit);
52
+ const nextCursor = snap.docs.length > limit && docs.length > 0 ? encodeCursor(docs[docs.length - 1].id) : null;
53
+ return {
54
+ docs,
55
+ nextCursor
56
+ };
57
+ }
58
+ export async function readJsonBody(request) {
59
+ try {
60
+ const body = await request.json();
61
+ return body && typeof body === 'object' ? body : {};
62
+ } catch (unused) {
63
+ return {};
64
+ }
65
+ }
66
+ /**
67
+ * Take an exclusive claim on one write attempt (AGL-1709), and translate the
68
+ * transport-agnostic result into a v1 `Response`.
69
+ *
70
+ * The claim itself is the shared `claimAttempt` (AGL-1697, `220217133`) — an
71
+ * unconditional `create()`, where Firestore's rejection on an existing document
72
+ * IS the dedupe primitive. This resource used to run its own read-then-create,
73
+ * which is the exact race the mechanism exists to prevent: two concurrent
74
+ * requests carrying one key both read "no prior key", both fall through, both
75
+ * create. Worse than no check at all in one respect, because it made the path
76
+ * look protected. Adopting the shared claim rather than fixing the local copy
77
+ * is the point — POS, refunds and the REST API now answer this question once.
78
+ *
79
+ * `scopeId` carries the ORG as well as the dataset. The helper hashes
80
+ * `{kind}:{scopeId}:{key}` and its plugin callers pass a globally unique host
81
+ * id, but a dataset id is only meaningful under its org — and the claim stores
82
+ * the response body, so a cross-org digest collision would replay one tenant's
83
+ * record to another. The org belongs in the digest, not just in the swept
84
+ * field. Scoping by the DATASET is what `apps/docs/api/conventions.md` already
85
+ * publishes ("replay is looked up within the dataset you're posting to"); the
86
+ * old org-only digest only reached that outcome by accident, by reading a
87
+ * `recordId` that did not exist in the other dataset and falling through — and
88
+ * having fallen through, its key write failed and was swallowed, so the key
89
+ * never deduped there at all.
90
+ *
91
+ * NOTE: the digest changes, so keys stored under the old `orgId:key` shape are
92
+ * orphaned and a retry straddling this deploy creates a duplicate. Accepted —
93
+ * the API is pre-beta and the alternative is carrying the race forever.
94
+ *
95
+ * `kind` separates the create's claims from the delete's (AGL-1710). The
96
+ * helper hashes it into the digest for exactly this reason, and the two record
97
+ * different response BODIES — a record view versus a `{ deleted: true }`
98
+ * receipt. Sharing one namespace would let a key reused across both operations
99
+ * replay the create's record to a delete, which a client parses as a success
100
+ * and which no amount of documentation makes safe. `POST` keeps `records`
101
+ * verbatim: changing its digest orphans keys in flight, and AGL-1709 already
102
+ * paid that cost once.
103
+ *
104
+ * `scopeSuffix` is the object the key is scoped WITHIN — a dataset id for the
105
+ * record operations and for `DELETE /v1/datasets/{id}`, the site for
106
+ * `DELETE …/form-submissions/{id}`, and `*` — the ORGANIZATION — for
107
+ * `POST /v1/datasets` (AGL-2126), where there is no dataset id yet because
108
+ * the request is what creates one, and for both contact writes (AGL-2276),
109
+ * where the organization genuinely IS the containing object: contacts are
110
+ * org-scoped (AGL-237), so there is no site or parent collection to scope to
111
+ * and pretending otherwise would invent a boundary the data does not have.
112
+ * `*` cannot collide with a resource id: `createResourceUid()` never emits
113
+ * one, and the `kind` is separate anyway. The suffix is not the whole story —
114
+ * `kind` is hashed in too, which is what keeps a create's key from replaying
115
+ * into a delete, and what keeps `contacts` and `contact-deletes` apart while
116
+ * they share a suffix.
117
+ *
118
+ * This paragraph used to say `*` covered "the two that act on the dataset
119
+ * collection itself", which the delete has never done — it passes
120
+ * `datasetRef.id`. The published contract in `apps/docs/api/conventions.md` was
121
+ * written from this comment and inherited the error (AGL-2218), which is the
122
+ * argument for reading the call rather than the docblock above it.
123
+ */ export async function claimWrite(ctx, scopeSuffix, key, kind) {
124
+ const result = await claimAttempt(ctx.firestore, {
125
+ kind,
126
+ scopeId: `${ctx.orgId}:${scopeSuffix}`,
127
+ // The field `eraseOrgIdempotencyKeys` sweeps on (AGL-1448).
128
+ orgId: ctx.orgId,
129
+ key: key != null ? key : '',
130
+ busyMessage: 'A request with this Idempotency-Key is still in progress'
131
+ });
132
+ if ('claim' in result) return result;
133
+ // The helper is transport-agnostic and answers `{ status, body }`; its
134
+ // plugin callers hand that straight to a pages-API `res.json()`. v1 publishes
135
+ // a `{ error: { type, message, code } }` envelope that clients branch on, so
136
+ // the refusal is rebuilt here rather than leaking the helper's bare
137
+ // `{ error: '<sentence>' }` onto a documented surface.
138
+ if (result.replay.status === 409) {
139
+ return {
140
+ replay: ApiErrors.conflict({
141
+ message: 'A request with this Idempotency-Key is still in progress',
142
+ code: 'idempotency_in_progress',
143
+ headers: ctx.headers
144
+ })
145
+ };
146
+ }
147
+ // Settled: replay the ORIGINAL response, from the stored body rather than a
148
+ // fresh read — which is what makes it survive a record since edited or
149
+ // deleted. The old lookup replayed only while the record still existed and
150
+ // fell through to a SECOND create otherwise.
151
+ return {
152
+ replay: apiJson(result.replay.body, {
153
+ status: result.replay.status,
154
+ headers: ctx.headers
155
+ })
156
+ };
157
+ }
158
+ // ── Usage ───────────────────────────────────────────────────────────────────
159
+ /**
160
+ * One metered dimension, as published (AGL-2277).
161
+ *
162
+ * `included`/`remaining` are `null` for an UNLIMITED band rather than the
163
+ * sentinel itself: `UNLIMITED` is `Number.POSITIVE_INFINITY`, which
164
+ * `JSON.stringify` silently turns into `null` anyway — so the choice is
165
+ * between a `null` that means "unlimited" on purpose and the same `null`
166
+ * arriving by accident, indistinguishable from a bug. Stated explicitly, and
167
+ * documented, so an integrator can branch on it.
168
+ *
169
+ * `metered` is the field that actually answers "what happens when I cross
170
+ * this" — true means the excess bills, false means the next call is refused.
171
+ * It is read off the plan's own overage rate through the `check*Quota`
172
+ * helpers rather than re-derived here, because a second copy of that rule
173
+ * would drift from the one the enforcement path uses, and this endpoint's
174
+ * whole value is telling a customer what the enforcement path will do.
175
+ */ export function usageBand(used, included, remaining, overageRateUsd) {
176
+ const unlimited = !Number.isFinite(included);
177
+ return {
178
+ used,
179
+ included: unlimited ? null : included,
180
+ remaining: unlimited ? null : remaining,
181
+ metered: overageRateUsd !== null
182
+ };
183
+ }
184
+ // ── Ownership ───────────────────────────────────────────────────────────────
185
+ /** Whether `hostId` is one of the authenticated organization's own sites. */ export function orgOwnsHost(ctx, hostId) {
186
+ var _ctx_org_hosts;
187
+ const hosts = (_ctx_org_hosts = ctx.org.hosts) != null ? _ctx_org_hosts : {};
188
+ return Boolean(hosts[hostId]);
189
+ }
190
+
191
+ //# sourceMappingURL=api-v1-kit.js.map