@cosmoops/connect-sdk 0.7.0 → 0.15.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 (154) hide show
  1. package/CHANGELOG.md +662 -0
  2. package/README.md +376 -124
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +59 -5
  6. package/dist/cjs/client/components/ai-format.js +135 -0
  7. package/dist/cjs/client/components/ai-widgets.js +232 -0
  8. package/dist/cjs/client/components/app-shell.js +80 -45
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +353 -0
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +42 -1
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +50 -24
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +122 -40
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  29. package/dist/cjs/client/index.js +98 -3
  30. package/dist/cjs/client/provider.js +100 -2
  31. package/dist/cjs/client/use-checkout.js +9 -5
  32. package/dist/cjs/errors.js +1 -1
  33. package/dist/cjs/index.js +11 -1
  34. package/dist/cjs/next/index.js +2 -2
  35. package/dist/cjs/server/client.js +25 -2
  36. package/dist/cjs/server/handler.js +237 -16
  37. package/dist/cjs/server/http.js +52 -26
  38. package/dist/cjs/server/resources/acting-as.js +16 -0
  39. package/dist/cjs/server/resources/ai.js +15 -2
  40. package/dist/cjs/server/resources/data.js +374 -0
  41. package/dist/cjs/server/resources/entitlements.js +41 -0
  42. package/dist/cjs/server/resources/identity.js +9 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store.js +152 -0
  48. package/dist/cjs/ui/empty-state.js +28 -0
  49. package/dist/cjs/ui/index.js +44 -0
  50. package/dist/cjs/ui/page-header.js +27 -0
  51. package/dist/cjs/ui/page-shell.js +44 -0
  52. package/dist/cjs/ui/section.js +20 -0
  53. package/dist/cjs/ui/setting-row.js +14 -0
  54. package/dist/cjs/ui/status-chip.js +17 -0
  55. package/dist/esm/cli/doctor.js +3 -3
  56. package/dist/esm/cli/init.js +3 -3
  57. package/dist/esm/client/components/access-gate.js +60 -6
  58. package/dist/esm/client/components/ai-format.js +129 -0
  59. package/dist/esm/client/components/ai-widgets.js +223 -0
  60. package/dist/esm/client/components/app-shell.js +85 -50
  61. package/dist/esm/client/components/apps-grid.js +3 -2
  62. package/dist/esm/client/components/ask-ai.js +350 -0
  63. package/dist/esm/client/components/confirm-dialog.js +37 -0
  64. package/dist/esm/client/components/cursor-pagination.js +19 -0
  65. package/dist/esm/client/components/filter-tabs.js +21 -0
  66. package/dist/esm/client/components/grievance-notice.js +27 -0
  67. package/dist/esm/client/components/link-button.js +17 -10
  68. package/dist/esm/client/components/logo.js +4 -3
  69. package/dist/esm/client/components/nav.js +41 -1
  70. package/dist/esm/client/components/plan-pricing.js +25 -0
  71. package/dist/esm/client/components/plans.js +2 -2
  72. package/dist/esm/client/components/platform-gate.js +45 -0
  73. package/dist/esm/client/components/shell-context.js +141 -0
  74. package/dist/esm/client/components/side-nav.js +49 -22
  75. package/dist/esm/client/components/sidebar.js +120 -0
  76. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  77. package/dist/esm/client/components/subscription.js +2 -2
  78. package/dist/esm/client/components/user-menu.js +121 -41
  79. package/dist/esm/client/components/workspace-switcher.js +97 -0
  80. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  81. package/dist/esm/client/index.js +58 -5
  82. package/dist/esm/client/provider.js +94 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/next/index.js +2 -2
  87. package/dist/esm/server/client.js +25 -2
  88. package/dist/esm/server/handler.js +237 -16
  89. package/dist/esm/server/http.js +52 -26
  90. package/dist/esm/server/resources/acting-as.js +13 -0
  91. package/dist/esm/server/resources/ai.js +15 -2
  92. package/dist/esm/server/resources/data.js +369 -0
  93. package/dist/esm/server/resources/entitlements.js +41 -0
  94. package/dist/esm/server/resources/identity.js +9 -3
  95. package/dist/esm/server/resources/kv.js +80 -0
  96. package/dist/esm/server/resources/platform.js +39 -0
  97. package/dist/esm/server/resources/sessions.js +1 -1
  98. package/dist/esm/server/resources/workspaces.js +70 -0
  99. package/dist/esm/store.js +141 -0
  100. package/dist/esm/ui/empty-state.js +25 -0
  101. package/dist/esm/ui/index.js +34 -0
  102. package/dist/esm/ui/page-header.js +24 -0
  103. package/dist/esm/ui/page-shell.js +41 -0
  104. package/dist/esm/ui/section.js +17 -0
  105. package/dist/esm/ui/setting-row.js +11 -0
  106. package/dist/esm/ui/status-chip.js +14 -0
  107. package/dist/theme.css +87 -0
  108. package/dist/types/client/components/access-gate.d.ts +17 -3
  109. package/dist/types/client/components/ai-format.d.ts +24 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  111. package/dist/types/client/components/app-shell.d.ts +85 -31
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +73 -0
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +27 -3
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +28 -14
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +70 -23
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  129. package/dist/types/client/index.d.ts +36 -7
  130. package/dist/types/client/provider.d.ts +149 -3
  131. package/dist/types/client/use-checkout.d.ts +8 -4
  132. package/dist/types/errors.d.ts +1 -1
  133. package/dist/types/index.d.ts +2 -0
  134. package/dist/types/next/index.d.ts +1 -1
  135. package/dist/types/server/client.d.ts +25 -2
  136. package/dist/types/server/http.d.ts +14 -3
  137. package/dist/types/server/index.d.ts +4 -0
  138. package/dist/types/server/resources/acting-as.d.ts +11 -0
  139. package/dist/types/server/resources/ai.d.ts +9 -1
  140. package/dist/types/server/resources/data.d.ts +258 -0
  141. package/dist/types/server/resources/entitlements.d.ts +33 -1
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store.d.ts +96 -0
  146. package/dist/types/types.d.ts +491 -13
  147. package/dist/types/ui/empty-state.d.ts +34 -0
  148. package/dist/types/ui/index.d.ts +40 -0
  149. package/dist/types/ui/page-header.d.ts +34 -0
  150. package/dist/types/ui/page-shell.d.ts +39 -0
  151. package/dist/types/ui/section.d.ts +26 -0
  152. package/dist/types/ui/setting-row.d.ts +18 -0
  153. package/dist/types/ui/status-chip.d.ts +36 -0
  154. package/package.json +13 -3
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataResource = void 0;
4
+ exports.storePassthrough = storePassthrough;
5
+ const errors_js_1 = require("../../errors.js");
6
+ const http_js_1 = require("../http.js");
7
+ const acting_as_js_1 = require("./acting-as.js");
8
+ /**
9
+ * Your application's own document store - a dedicated Firestore database that
10
+ * only Connect can reach.
11
+ *
12
+ * You hold no Firebase credential and write no security rules. Collections are
13
+ * declared in the Console; an undeclared name is refused rather than created,
14
+ * and `workspaceId` is a path segment rather than a filter, so a query cannot
15
+ * cross tenants.
16
+ *
17
+ * **A query needs an index for its shape, and there are two ways to have one.**
18
+ * Firestore maintains a single-field index automatically, which covers one
19
+ * filter, or an `orderBy` on that same field. Anything wider - two filters, or
20
+ * a filter ordered by a different field - needs a composite index **declared
21
+ * on your application's Store tab**, which Connect then builds in your
22
+ * database. A query with no matching index is refused up front, naming the
23
+ * index to declare, rather than failing later with a Firestore index error
24
+ * pointing at a console you cannot open.
25
+ *
26
+ * **Dates are ISO-8601 strings.** The store writes what you send it, and JSON
27
+ * has no date type, so a `Date` must be sent as `toISOString()`. That is not a
28
+ * compromise: UTC ISO strings sort lexicographically in chronological order,
29
+ * so ordering and range filters on them behave exactly as they would on a
30
+ * timestamp - and Connect's own `_createdAt`/`_updatedAt` come back in the
31
+ * same form.
32
+ *
33
+ * **`workspaceId` is omitted for master data.** A collection declared
34
+ * application-scoped in the Console belongs to no workspace - one copy, owned
35
+ * by you, read by every tenant. Those are written with your API key only,
36
+ * never on behalf of a user: a write permission is something a *workspace*
37
+ * grants, so honouring one on shared data would give one tenant's admin reach
38
+ * over every other tenant's reference data.
39
+ *
40
+ * **`asUser` is how you act on behalf of a signed-in user.** Pass the opaque
41
+ * `sessionToken` you were given at sign-in and Connect resolves the user and
42
+ * their workspace from it - checking the collection's declared permission
43
+ * against what that member actually holds, and **ignoring `workspaceId`
44
+ * entirely**. Without it you are the application itself: your API key's scopes
45
+ * are the only gate, and the per-user permissions do not apply. The bridge
46
+ * (`createConnectHandler`) always passes it, which is what makes the browser
47
+ * path safe.
48
+ */
49
+ class DataResource {
50
+ transport;
51
+ constructor(transport) {
52
+ this.transport = transport;
53
+ }
54
+ /** Reads one document. Throws `ConnectError` with status 404 when it does not exist. */
55
+ get(input) {
56
+ return (0, http_js_1.request)(this.transport, {
57
+ method: "GET",
58
+ path: `/data/${(0, http_js_1.pathSegment)(input.collection, "collection")}/${(0, http_js_1.pathSegment)(input.id, "id")}`,
59
+ query: { workspaceId: input.workspaceId },
60
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
61
+ });
62
+ }
63
+ /**
64
+ * Lists a collection, one page at a time.
65
+ *
66
+ * `filters` are ANDed. More than one, or one combined with an `orderBy` on a
67
+ * different field, needs a composite index declared on the Store tab - see
68
+ * the class note above.
69
+ *
70
+ * `limit` defaults to 50 and caps at 200. Page by passing the previous
71
+ * page's `nextCursor`; a `null` `nextCursor` is the end of the collection.
72
+ */
73
+ query(input) {
74
+ return (0, http_js_1.request)(this.transport, {
75
+ method: "GET",
76
+ path: `/data/${(0, http_js_1.pathSegment)(input.collection, "collection")}`,
77
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
78
+ query: {
79
+ workspaceId: input.workspaceId,
80
+ // JSON rather than flattened params: a filter *value* is typed, and
81
+ // `42`, `"42"` and `true` are three different queries that a flattened
82
+ // form collapses into one.
83
+ filters: input.filters?.length ? JSON.stringify(input.filters) : undefined,
84
+ orderBy: input.orderBy ? JSON.stringify(input.orderBy) : undefined,
85
+ limit: input.limit,
86
+ cursor: input.cursor ?? undefined,
87
+ },
88
+ });
89
+ }
90
+ /**
91
+ * Counts, sums and averages a filtered collection - **in Firestore**, not by
92
+ * reading the rows back and adding them up.
93
+ *
94
+ * That distinction is the whole point. An aggregation is billed by the index
95
+ * entries it scans rather than by the documents it would have returned, so a
96
+ * dashboard tile over ten thousand rows costs a handful of reads and stays
97
+ * flat as the collection grows. Paging the rows into your own process to sum
98
+ * them is an unbounded read, and it is what every integration does when
99
+ * there is no aggregate call to reach for.
100
+ *
101
+ * ```ts
102
+ * const { values } = await connect.data.aggregate({
103
+ * workspaceId,
104
+ * collection: "deals",
105
+ * filters: [{ field: "outcome", op: "==", value: "won" }],
106
+ * aggregations: [
107
+ * { op: "count", alias: "count" },
108
+ * { op: "sum", field: "value", alias: "total" },
109
+ * ],
110
+ * });
111
+ * ```
112
+ *
113
+ * A `sum` or `avg` over nothing answers `null` rather than `0`, because
114
+ * "nothing matched" and "the total is zero" are different facts.
115
+ *
116
+ * The index it needs is the one its **filters** need plus the aggregated
117
+ * field - Firestore reads a sum out of the index rather than out of the
118
+ * documents.
119
+ *
120
+ * There is no `orderBy`, `limit` or `cursor`: the aggregate of an ordered
121
+ * page is the aggregate of the whole filtered set.
122
+ */
123
+ aggregate(input) {
124
+ return (0, http_js_1.request)(this.transport, {
125
+ method: "GET",
126
+ path: `/data-aggregate/${(0, http_js_1.pathSegment)(input.collection, "collection")}`,
127
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
128
+ query: {
129
+ workspaceId: input.workspaceId,
130
+ aggregations: JSON.stringify(input.aggregations.map((aggregation) => ({
131
+ op: aggregation.op,
132
+ field: aggregation.field ?? null,
133
+ alias: aggregation.alias,
134
+ }))),
135
+ filters: input.filters?.length ? JSON.stringify(input.filters) : undefined,
136
+ },
137
+ });
138
+ }
139
+ /**
140
+ * Reads one collection across **every workspace at once** - your own
141
+ * back-office view of your whole customer base. Each row carries the
142
+ * `workspaceId` it came from.
143
+ *
144
+ * Needs the `data:read:all` scope, and the collection must be marked
145
+ * cross-workspace in the Console. There is deliberately **no `asUser`**: a
146
+ * scan is never available on behalf of a signed-in user, because a workspace
147
+ * admin is an admin of their own workspace and not of your other customers.
148
+ *
149
+ * Filtering and ordering follow the same index rules as `query`, and
150
+ * additionally need collection-group scope. Connect creates those from the
151
+ * declaration - the single-field ones when you mark a field queryable, and a
152
+ * group-scoped copy of every composite index the collection declares. A scan
153
+ * issued in the minutes after that declaration may fail while the index
154
+ * builds.
155
+ */
156
+ scan(input) {
157
+ return (0, http_js_1.request)(this.transport, {
158
+ method: "GET",
159
+ path: `/data-scan/${(0, http_js_1.pathSegment)(input.collection, "collection")}`,
160
+ query: {
161
+ filters: input.filters?.length ? JSON.stringify(input.filters) : undefined,
162
+ orderBy: input.orderBy ? JSON.stringify(input.orderBy) : undefined,
163
+ limit: input.limit,
164
+ cursor: input.cursor ?? undefined,
165
+ },
166
+ });
167
+ }
168
+ /**
169
+ * Creates a document. Pass `id` to choose it, or omit it and one is
170
+ * allocated. An `id` that already exists throws with status 409 - use
171
+ * `replace` to overwrite deliberately.
172
+ */
173
+ create(input) {
174
+ return (0, http_js_1.request)(this.transport, {
175
+ method: "POST",
176
+ path: `/data/${(0, http_js_1.pathSegment)(input.collection, "collection")}`,
177
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
178
+ json: { workspaceId: input.workspaceId, id: input.id ?? null, data: input.data },
179
+ });
180
+ }
181
+ /** Replaces every field, creating the document if it does not exist. `_createdAt`/`_createdBy` survive. */
182
+ replace(input) {
183
+ return this.write(input, "replace");
184
+ }
185
+ /** Merges top-level fields into an existing document. Throws with status 404 if it does not exist. */
186
+ update(input) {
187
+ return this.write(input, "merge");
188
+ }
189
+ /** Idempotent - deleting a document that is not there succeeds. */
190
+ async delete(input) {
191
+ await (0, http_js_1.request)(this.transport, {
192
+ method: "DELETE",
193
+ path: `/data/${(0, http_js_1.pathSegment)(input.collection, "collection")}/${(0, http_js_1.pathSegment)(input.id, "id")}`,
194
+ query: { workspaceId: input.workspaceId },
195
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
196
+ });
197
+ }
198
+ /**
199
+ * Applies a set of writes atomically, across as many of your collections as
200
+ * they name. All of them land, or none.
201
+ *
202
+ * This is the store's answer to a database transaction, and the shape is
203
+ * deliberate: **you never hold one**. A transaction handle passed across an
204
+ * HTTP boundary is a server-side lock whose duration your network controls,
205
+ * with Firestore's sixty-second ceiling and lock contention as the failure
206
+ * mode. Instead you read, decide, and commit *conditional on nothing having
207
+ * moved* - Connect runs the whole set as one short transaction on its side.
208
+ *
209
+ * ```ts
210
+ * await connect.data.commit({
211
+ * workspaceId,
212
+ * operations: [
213
+ * { op: "create", collection: "ledger", id, data: entry },
214
+ * {
215
+ * op: "merge",
216
+ * collection: "funds",
217
+ * id: entry.fundId,
218
+ * data: { balancePaise: increment(entry.amountPaise) },
219
+ * },
220
+ * ],
221
+ * });
222
+ * ```
223
+ *
224
+ * A precondition that no longer holds throws `ConnectError` with status 409
225
+ * and **writes nothing** - see `transaction`, which re-reads and retries for
226
+ * you. Note that the example above needs no precondition at all: an
227
+ * `increment` is commutative, so concurrent commits both land correctly.
228
+ *
229
+ * Returns the id each operation wrote, in order - which is how you learn the
230
+ * id a `create` without one was given.
231
+ */
232
+ commit(input) {
233
+ return (0, http_js_1.request)(this.transport, {
234
+ method: "POST",
235
+ path: "/data-commit",
236
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
237
+ json: {
238
+ workspaceId: input.workspaceId,
239
+ operations: input.operations.map((operation) => ({
240
+ op: operation.op,
241
+ collection: operation.collection,
242
+ id: operation.id ?? null,
243
+ data: operation.data ?? null,
244
+ precondition: operation.precondition ?? null,
245
+ })),
246
+ },
247
+ });
248
+ }
249
+ /**
250
+ * Read, decide, commit - retried when the world moved underneath you.
251
+ *
252
+ * The optimistic-concurrency loop, written once. `build` reads whatever it
253
+ * needs and returns the operations to apply; if any precondition among them
254
+ * has gone stale by the time Connect looks, the commit is rejected whole and
255
+ * `build` runs again against fresh reads.
256
+ *
257
+ * ```ts
258
+ * await connect.data.transaction(async () => {
259
+ * const fund = await connect.data.get({ workspaceId, collection: "funds", id });
260
+ * if (!fund.data.active) throw new Error("That fund is closed.");
261
+ * return [
262
+ * {
263
+ * op: "merge",
264
+ * collection: "funds",
265
+ * id,
266
+ * data: { balancePaise: increment(amount) },
267
+ * precondition: unchangedSince(fund),
268
+ * },
269
+ * ];
270
+ * });
271
+ * ```
272
+ *
273
+ * **Only a 409 is retried.** A 403 or a validation error means the operation
274
+ * was wrong rather than late, and retrying it would turn one clear failure
275
+ * into `attempts` identical ones. An error thrown by `build` itself - the
276
+ * closed-fund check above - is not retried either: it is a decision, not a
277
+ * collision.
278
+ *
279
+ * `build` must be free of side effects outside the returned operations,
280
+ * because it can run more than once. That is the price of not holding a
281
+ * lock, and it is the same contract Firestore's own `runTransaction` states
282
+ * for the same reason.
283
+ */
284
+ async transaction(build, options = {}) {
285
+ const attempts = options.attempts ?? 4;
286
+ let lastConflict;
287
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
288
+ const operations = await build();
289
+ try {
290
+ return await this.commit({
291
+ workspaceId: options.workspaceId,
292
+ operations,
293
+ asUser: options.asUser,
294
+ });
295
+ }
296
+ catch (error) {
297
+ if (!isConflict(error))
298
+ throw error;
299
+ lastConflict = error;
300
+ }
301
+ }
302
+ throw lastConflict;
303
+ }
304
+ write(input, mode) {
305
+ return (0, http_js_1.request)(this.transport, {
306
+ method: "PUT",
307
+ path: `/data/${(0, http_js_1.pathSegment)(input.collection, "collection")}/${(0, http_js_1.pathSegment)(input.id, "id")}`,
308
+ headers: { "x-store-write-mode": mode, ...(0, acting_as_js_1.actingAs)(input.asUser) },
309
+ json: { workspaceId: input.workspaceId, data: input.data },
310
+ });
311
+ }
312
+ }
313
+ exports.DataResource = DataResource;
314
+ /**
315
+ * A stale precondition, as opposed to any other refusal.
316
+ *
317
+ * Matched on the status rather than on the message, which is not part of any
318
+ * contract - and narrowly, because retrying anything else turns one honest
319
+ * failure into four.
320
+ */
321
+ function isConflict(error) {
322
+ return error instanceof errors_js_1.ConnectError && error.status === 409;
323
+ }
324
+ /**
325
+ * A raw store passthrough, for `createConnectHandler` and nothing else.
326
+ *
327
+ * The bridge relays a browser's `/data/*` and `/kv/*` request without
328
+ * reconstructing it into a typed call - it has a path, a body and a session
329
+ * token, and re-parsing them into `get`/`query`/`create` only to serialise
330
+ * them again would be a second implementation of the same URL grammar, drifting
331
+ * from this one.
332
+ *
333
+ * **Deliberately not a generic proxy.** The path is refused unless it is one
334
+ * of the store endpoints, so this cannot become a way to reach the rest of the
335
+ * application API under a browser's control - which is the property the bridge
336
+ * has always been careful to keep.
337
+ *
338
+ * `/data-scan/` is deliberately **not** in that set. A scan crosses every
339
+ * tenant, and Connect refuses one made on behalf of a user anyway - but a
340
+ * browser should not be able to *ask*, so the refusal happens here rather than
341
+ * a round trip away.
342
+ */
343
+ /**
344
+ * The store paths a browser may reach through the bridge: documents, the
345
+ * key/value store, aggregations, and atomic commits. Anchored, so a path like
346
+ * `/data-scan/tickets` cannot slip through on a prefix match.
347
+ */
348
+ const STORE_PATHS = /^\/(?:data|kv|data-aggregate)\/|^\/data-commit$/;
349
+ // `async` so the refusal below arrives as a rejected promise rather than a
350
+ // synchronous throw. A function typed `Promise<Response>` that sometimes
351
+ // throws before returning one is the kind of thing a caller's `.catch()`
352
+ // misses.
353
+ async function storePassthrough(transport, spec) {
354
+ // The query string is stripped first: the bridge appends `?workspaceId=…`
355
+ // to every relayed path, and an anchored match against the whole thing would
356
+ // reject `/data-commit?workspaceId=…` for having one.
357
+ if (!STORE_PATHS.test(spec.path.split("?")[0] ?? "")) {
358
+ throw new errors_js_1.ConnectError({
359
+ status: 0,
360
+ code: "validation_error",
361
+ message: `storePassthrough only serves the store endpoints, not "${spec.path}"`,
362
+ endpoint: "client",
363
+ });
364
+ }
365
+ return (0, http_js_1.request)(transport, {
366
+ method: spec.method,
367
+ path: spec.path,
368
+ json: spec.json,
369
+ headers: spec.headers,
370
+ // The bridge hands Connect's response straight back to the browser, status
371
+ // and all, rather than parsing and re-encoding it.
372
+ raw: true,
373
+ });
374
+ }
@@ -45,5 +45,46 @@ class EntitlementsResource {
45
45
  const result = await this.getSubscription(workspaceId);
46
46
  return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
47
47
  }
48
+ /**
49
+ * One page of the workspaces subscribed to your application, newest first.
50
+ *
51
+ * The inverse of `getSubscription`, which asks about one workspace you
52
+ * already know. This asks who your subscribers *are* - what a partner
53
+ * dashboard, an MRR total or a churn report is built on.
54
+ *
55
+ * Rate-limited far more tightly than the per-workspace read (10/min per
56
+ * key, against 60): it walks your whole subscriber base and is a reporting
57
+ * call, not a per-request gate. Use `getSubscription` in a hot path.
58
+ *
59
+ * Your application is resolved from the API key, so there is no parameter
60
+ * for it and no way to ask about anyone else's.
61
+ */
62
+ listSubscribers(options = {}) {
63
+ return (0, http_js_1.request)(this.transport, {
64
+ method: "GET",
65
+ path: "/subscriptions",
66
+ query: { status: options.status, before: options.before, limit: options.limit },
67
+ });
68
+ }
69
+ /**
70
+ * Every subscriber, paged through for you.
71
+ *
72
+ * An async generator rather than an array: a large subscriber base is
73
+ * exactly where an accidental full materialisation hurts, and `for await`
74
+ * reads the same as a loop over one page.
75
+ */
76
+ async *iterateSubscribers(options = {}) {
77
+ let before;
78
+ do {
79
+ const page = await this.listSubscribers({
80
+ status: options.status,
81
+ limit: options.pageSize,
82
+ before,
83
+ });
84
+ for (const item of page.items)
85
+ yield item;
86
+ before = page.nextBefore ?? undefined;
87
+ } while (before);
88
+ }
48
89
  }
49
90
  exports.EntitlementsResource = EntitlementsResource;
@@ -70,7 +70,10 @@ class IdentityResource {
70
70
  sessionTokenExpiresIn: wire.session_token_expires_in,
71
71
  user: wire.user,
72
72
  subscription: wire.subscription,
73
- applicationRole: wire.applicationRole,
73
+ role: wire.role,
74
+ permissions: wire.permissions,
75
+ staff: wire.staff,
76
+ audience: wire.audience,
74
77
  };
75
78
  }
76
79
  /**
@@ -140,7 +143,10 @@ class IdentityResource {
140
143
  return {
141
144
  user: wire.user,
142
145
  subscription: wire.subscription,
143
- applicationRole: wire.applicationRole,
146
+ role: wire.role,
147
+ permissions: wire.permissions,
148
+ staff: wire.staff,
149
+ audience: wire.audience,
144
150
  sessionExpiresAt: wire.session_expires_at,
145
151
  };
146
152
  }
@@ -181,7 +187,7 @@ class IdentityResource {
181
187
  throw new errors_js_1.ConnectError({
182
188
  status: 0,
183
189
  code: "missing_client_id",
184
- message: "identity.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this application's Integration tab, under Connect client.",
190
+ message: "identity.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this application's Connect client section.",
185
191
  endpoint: "identity",
186
192
  });
187
193
  }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KvResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ const acting_as_js_1 = require("./acting-as.js");
6
+ /**
7
+ * Your application's key/value store - small values addressed by namespace and
8
+ * key, per workspace.
9
+ *
10
+ * Use it for what a document store is bad at: a feature flag, a cached
11
+ * computation, a counter, a draft nobody needs to query. Anything you want to
12
+ * *find* rather than *fetch* belongs in `data`.
13
+ *
14
+ * Namespaces are declared in the Console exactly like collections. Keys are
15
+ * `[A-Za-z0-9_-]` up to 64 characters, and values are JSON up to 128 KB.
16
+ *
17
+ * `asUser` behaves exactly as it does on `DataResource` - pass a signed-in
18
+ * user's `sessionToken` and Connect derives the user and workspace from it,
19
+ * checking the namespace's declared permission against their live roles.
20
+ */
21
+ class KvResource {
22
+ transport;
23
+ constructor(transport) {
24
+ this.transport = transport;
25
+ }
26
+ /**
27
+ * Reads one key. Throws `ConnectError` with status 404 when the key is
28
+ * absent **or expired** - expiry is evaluated on read, so there is no window
29
+ * where a stale value is still served.
30
+ */
31
+ get(input) {
32
+ return (0, http_js_1.request)(this.transport, {
33
+ method: "GET",
34
+ path: this.path(input.namespace, input.key),
35
+ query: { workspaceId: input.workspaceId },
36
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
37
+ });
38
+ }
39
+ /**
40
+ * Pages a namespace in key order.
41
+ *
42
+ * Expired entries are filtered out after paging, so a page can come back
43
+ * shorter than `limit` while still having a `nextCursor` - treat
44
+ * `nextCursor` as the only signal that more keys exist, never an empty page.
45
+ */
46
+ list(input) {
47
+ return (0, http_js_1.request)(this.transport, {
48
+ method: "GET",
49
+ path: `/kv/${(0, http_js_1.pathSegment)(input.namespace, "namespace")}`,
50
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
51
+ query: {
52
+ workspaceId: input.workspaceId,
53
+ limit: input.limit,
54
+ cursor: input.cursor ?? undefined,
55
+ },
56
+ });
57
+ }
58
+ /** Sets a value, replacing whatever was there. `ttlSeconds` expires it; omit for no expiry. */
59
+ set(input) {
60
+ return (0, http_js_1.request)(this.transport, {
61
+ method: "PUT",
62
+ path: this.path(input.namespace, input.key),
63
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
64
+ json: {
65
+ workspaceId: input.workspaceId,
66
+ value: input.value,
67
+ ttlSeconds: input.ttlSeconds ?? null,
68
+ },
69
+ });
70
+ }
71
+ /** Idempotent - deleting an absent key succeeds. */
72
+ async delete(input) {
73
+ await (0, http_js_1.request)(this.transport, {
74
+ method: "DELETE",
75
+ path: this.path(input.namespace, input.key),
76
+ query: { workspaceId: input.workspaceId },
77
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
78
+ });
79
+ }
80
+ path(namespace, key) {
81
+ return `/kv/${(0, http_js_1.pathSegment)(namespace, "namespace")}/${(0, http_js_1.pathSegment)(key, "key")}`;
82
+ }
83
+ }
84
+ exports.KvResource = KvResource;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlatformResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ /**
6
+ * Facts about the platform itself, rather than about a workspace or a user.
7
+ *
8
+ * One method today, and the resource exists anyway rather than hanging
9
+ * `getCompanyProfile` off `identity` or `workspaces`: neither of those is
10
+ * what it is about. This answers "who operates the thing my application runs
11
+ * on", which is a different subject from every other resource here, and a
12
+ * misfiled method is harder to find than an obvious one-method namespace.
13
+ */
14
+ class PlatformResource {
15
+ transport;
16
+ constructor(transport) {
17
+ this.transport = transport;
18
+ }
19
+ /**
20
+ * The operating entity's legal identity and its DPDP grievance channel.
21
+ *
22
+ * Takes no `workspaceId` and takes no `asUser` - the answer is the same for
23
+ * every tenant and every caller, because it is a fact about the platform.
24
+ *
25
+ * Returns the inner `companyProfile` rather than the envelope, matching
26
+ * `apps.list()` and `billing.listPlans()`: the wrapper is a wire detail, and
27
+ * every caller would destructure it on the next line.
28
+ *
29
+ * **Do not copy the result into your own configuration.** Cache it if you
30
+ * render it on a hot path - Connect already sends
31
+ * `s-maxage=3600, stale-while-revalidate=86400` - but the reason this is an
32
+ * endpoint at all is that a grievance officer copied into `.env` goes stale
33
+ * without anyone finding out.
34
+ */
35
+ async getCompanyProfile() {
36
+ const result = await (0, http_js_1.request)(this.transport, {
37
+ method: "GET",
38
+ path: "/company-profile",
39
+ });
40
+ return result.companyProfile;
41
+ }
42
+ }
43
+ exports.PlatformResource = PlatformResource;
@@ -72,7 +72,7 @@ class SessionsResource {
72
72
  throw new errors_js_1.ConnectError({
73
73
  status: 0,
74
74
  code: "missing_client_id",
75
- message: "sessions.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this application's Integration tab, under Connect client.",
75
+ message: "sessions.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this application's Connect client section.",
76
76
  endpoint: "sessions",
77
77
  });
78
78
  }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkspacesResource = void 0;
4
+ const acting_as_js_1 = require("./acting-as.js");
5
+ const http_js_1 = require("../http.js");
6
+ /**
7
+ * Which workspaces the signed-in user belongs to, moving between them, and who
8
+ * else is on the team.
9
+ *
10
+ * **Every call here needs `asUser`** - the signed-in user's opaque
11
+ * `sessionToken`. There is no application-wide form of any of these: they take
12
+ * no `userId` and no `workspaceId`, and resolve both from the token. That is
13
+ * what makes the whole module safe to reach from a browser through
14
+ * `createConnectHandler`, and it is why a partner cannot enumerate another
15
+ * tenant's team with their own API key.
16
+ */
17
+ class WorkspacesResource {
18
+ transport;
19
+ constructor(transport) {
20
+ this.transport = transport;
21
+ }
22
+ /**
23
+ * The workspaces this user could switch to.
24
+ *
25
+ * Rows carry `inAudience` and `subscribed` so a switcher can show a
26
+ * workspace it cannot select, with the reason. Hiding it instead reads as
27
+ * data loss to the person who owns it.
28
+ */
29
+ async list(options) {
30
+ const result = await (0, http_js_1.request)(this.transport, {
31
+ method: "GET",
32
+ path: "/workspaces",
33
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
34
+ });
35
+ return result.workspaces;
36
+ }
37
+ /**
38
+ * Moves this session to another workspace, returning a **new session
39
+ * token**. The one you passed is revoked.
40
+ *
41
+ * Persist the result before navigating - dropping it signs the user out,
42
+ * because the old token no longer works.
43
+ *
44
+ * Throws with status 404 when the workspace does not exist *or* the user is
45
+ * not a member (deliberately indistinguishable), and 403 when it is outside
46
+ * your audience, requires an MFA factor they have not enrolled, or is the
47
+ * workspace they are already on.
48
+ */
49
+ switch(input, options) {
50
+ return (0, http_js_1.request)(this.transport, {
51
+ method: "POST",
52
+ path: "/workspaces/switch",
53
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
54
+ json: input,
55
+ });
56
+ }
57
+ /**
58
+ * The team of the workspace this session is on.
59
+ *
60
+ * Members carry permissions already resolved for *your* application, not raw
61
+ * Connect roles - so you never reimplement Connect's role resolution, and
62
+ * you are told nothing about the customer's Connect account beyond what your
63
+ * application needs.
64
+ */
65
+ async members(options) {
66
+ const result = await (0, http_js_1.request)(this.transport, {
67
+ method: "GET",
68
+ path: "/workspaces/members",
69
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
70
+ });
71
+ return result.members;
72
+ }
73
+ }
74
+ exports.WorkspacesResource = WorkspacesResource;