@cosmoops/connect-sdk 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,109 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.36.0
7
+
8
+ ### A workspace's billing details can be read, so a tax id is typed once
9
+
10
+ `workspaces.billingProfile({ asUser })` returns the legal name, GSTIN,
11
+ registered address and entity form the customer gave CosmoOps so we can
12
+ invoice them.
13
+
14
+ It exists for one shape of problem: an application that prints documents - a
15
+ CRM's quotation letterhead - needs the same three facts about the same legal
16
+ entity, and the only way to get them was to ask the customer again. Two copies
17
+ of a GSTIN diverge, and on an Indian quotation the first two characters of that
18
+ GSTIN are what decide CGST+SGST versus IGST.
19
+
20
+ ```ts
21
+ const token = await getConnectSessionToken();
22
+ const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
23
+ // offer "Use my billing details" — then keep your own copy
24
+ ```
25
+
26
+ **It is a prefill source, not a shared record.** This is the *Bill To* side -
27
+ who we invoice - and your letterhead is the *Bill From* side. They are the same
28
+ facts for most customers without being the same field: a group that bills us
29
+ through a holding company while quoting under a trading subsidiary has to stay
30
+ expressible. So copy it on first setup and keep your copy authoritative. There
31
+ is deliberately **no write form** - an application must not edit the details
32
+ its own invoices are raised against.
33
+
34
+ **Nothing in it is verified.** It is self-declared in the customer's billing
35
+ settings, not the KYB record; no PAN check stands behind it. A convenience,
36
+ never evidence of identity.
37
+
38
+ **It needs `billing:read` on the signed-in member**, on top of `workspaces:read`
39
+ on your key. A tax identity belongs to whoever can already see it in Connect's
40
+ billing screens - an owner, admin or manager - and it cannot be granted through
41
+ a role on your application. So an ordinary member gets a `403` here where they
42
+ get a roster from `members()`: **treat that as a normal branch**, offer the
43
+ prefill to whoever can use it, and let everyone else type their own details.
44
+
45
+ `razorpayCustomerId` is not returned. It is a payment-provider handle no
46
+ partner has a use for.
47
+
48
+ ### `getConnectSessionToken()`, so `asUser` works from a Server Component
49
+
50
+ Every `asUser` call needs the caller's opaque session token, and there was no
51
+ way to obtain one inside a Server Component: `getConnectSession()` answers
52
+ *who* the caller is and deliberately does not hand back the credential. A page
53
+ that wanted its own workspace's roster had to reimplement the cookie read or
54
+ route the call through the browser.
55
+
56
+ ```tsx
57
+ const token = await getConnectSessionToken();
58
+ const members = token ? await connect.workspaces.members({ asUser: token }) : [];
59
+ ```
60
+
61
+ **It is the cookie, unverified** - no round trip, and no check that the session
62
+ is still live. A non-null answer means a cookie was present, not that it works;
63
+ Connect is what answers `401` for a revoked one. Use `getConnectSession()` for
64
+ "is somebody here" and this for "hand Connect the credential they arrived
65
+ with".
66
+
67
+ ### `SDK_VERSION` matched the published version again
68
+
69
+ It still read `0.35.0` after the version bump, so every request from this
70
+ release would have carried a `User-Agent` a version behind. `src/sdk.test.ts`
71
+ is what caught it, and is the reason the constant is pinned to
72
+ `package.json` rather than trusted.
73
+
74
+
75
+ One field, `ConnectWorkspaceMember.memberId`, without which no integration
76
+ could say who an appointment belongs to.
77
+
78
+ ### A diary and a roster could not be joined
79
+
80
+ `Booking.hostMemberIds` holds `WorkspaceMember.id` values, and so does
81
+ `listBookings`' `hostMemberId` filter. `workspaces.members()` returned
82
+ `userId` and no member id at all - so the two sets of ids had nothing in
83
+ common, and a partner could filter a diary by a value it had no way to read
84
+ back.
85
+
86
+ That made two obvious features unreachable rather than merely awkward: a host
87
+ column on a calendar, and narrowing a shared calendar to one person. Both fail
88
+ *quietly* if you reach for `userId` instead, because a filter nobody matches
89
+ returns an empty window, which reads as a free afternoon rather than a bug.
90
+
91
+ ```ts
92
+ const { members } = await connect.workspaces.members({ asUser });
93
+ const byMemberId = new Map(members.map((m) => [m.memberId, m]));
94
+
95
+ const { bookings } = await connect.scheduling.listBookings({ workspaceId, asUser });
96
+ bookings.map((b) => b.hostMemberIds.map((id) => byMemberId.get(id)?.displayName));
97
+ ```
98
+
99
+ The two ids are not interchangeable and neither is derived from the other:
100
+ `userId` identifies the person everywhere else in this API, `memberId` is what
101
+ the calendar speaks in. It is workspace-scoped and opaque - do not address a
102
+ member by it anywhere else.
103
+
104
+ Additive, so nothing breaks. `Booking.hostMemberIds`' own doc comment claimed
105
+ these were already the ids `workspaces.listMembers` returns, which was wrong
106
+ twice over - that method is `workspaces.members()`, and until now its rows
107
+ carried no id these could be matched against.
108
+
6
109
  ## 0.35.0
7
110
 
8
111
  One addition, `connectProviderProps()`, for a mistake both applications built
@@ -92,6 +195,27 @@ in the output. A note rather than a failure, so it cannot break a CI run: the
92
195
  prop may be threaded from the layout into a `"use client"` shell, which reading
93
196
  the layout cannot see.
94
197
 
198
+ ### The rail fits a ten-destination nav on a laptop
199
+
200
+ `AppShell`'s nav rows, its launcher and its secondary-nav rows are **36px**
201
+ rather than 40px, and the one region that scrolls (`ConnectSidebarBody`) carries
202
+ `py-1` rather than `py-2`. The two rows with a 32px glyph - the account row and
203
+ the workspace switcher - stay at 40px: there are exactly two of them, neither
204
+ grows with your nav, and both need the height (a 32px tile in a 36px row has 2px
205
+ of air, and the account row's name-over-email label measures ~32px on its own).
206
+
207
+ Ten destinations at 40px came to 436px of nav against 273px of fixed chrome,
208
+ which needs a 709px viewport to sit still - a 1366x768 screen has about 678px of
209
+ it, so the nav scrolled on a perfectly ordinary laptop. Four pixels a row is 48px
210
+ back across the twelve rows a real integration draws. 36px is still comfortably
211
+ past WCAG 2.5.8's 24px target minimum, with the row's full width as the hit area.
212
+
213
+ The glyph box is deliberately unchanged at 20px. It is a flex centring container
214
+ for an icon **you** supply at your own size, so shrinking it buys no vertical
215
+ space and only pushes the icon off the 32px centre the collapsed rail is built
216
+ on. If you pass an icon larger than 20px it will now sit in a slightly tighter
217
+ row; `size-5` remains the recommendation.
218
+
95
219
  ### Fixed: the User-Agent reported the wrong version
96
220
 
97
221
  `SDK_VERSION` was left at `0.34.0` when 0.34.1 shipped, so every request this
@@ -41,6 +41,7 @@ exports.cookieSessionStore = cookieSessionStore;
41
41
  exports.createConnectHandlers = createConnectHandlers;
42
42
  exports.GET = handle;
43
43
  exports.POST = handle;
44
+ exports.getConnectSessionToken = getConnectSessionToken;
44
45
  exports.getConnectSession = getConnectSession;
45
46
  exports.requireConnectSession = requireConnectSession;
46
47
  const react_1 = require("react");
@@ -352,6 +353,45 @@ async function readSession(request, options) {
352
353
  const cachedAmbientSession = (0, react_1.cache)(async () => {
353
354
  return readSession(await requestFromHeaders(), {});
354
355
  });
356
+ /**
357
+ * The caller's opaque Connect **session token**, or `null` when signed out.
358
+ *
359
+ * ## What it is for
360
+ *
361
+ * Every `asUser` call on the server client needs one - `workspaces.*`,
362
+ * `scheduling.*`, the `/data` store's per-user form - and until this existed
363
+ * there was no way to obtain it inside a Server Component. `getConnectSession`
364
+ * answers *who* the caller is and deliberately does not hand back the
365
+ * credential, so a page that wanted to read its own workspace's roster had to
366
+ * either reimplement the cookie read or route the call through the browser.
367
+ *
368
+ * ```tsx
369
+ * // app/settings/page.tsx
370
+ * const token = await getConnectSessionToken();
371
+ * const members = token ? await connect.workspaces.members({ asUser: token }) : [];
372
+ * ```
373
+ *
374
+ * ## It is the cookie, unverified
375
+ *
376
+ * This reads the session store and nothing else - **no round trip, and no
377
+ * check that the token is still live.** A token from a revoked or expired
378
+ * session is a string that looks exactly like a good one, and Connect is what
379
+ * answers `401` for it. So this is not a sign-in check: use
380
+ * `getConnectSession()` for "is somebody here", and this for "hand Connect the
381
+ * credential they arrived with". A non-null answer means a cookie was present,
382
+ * not that it works.
383
+ *
384
+ * Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
385
+ * a `<Suspense>` boundary like any other request-scoped read.
386
+ *
387
+ * Pass a `Request` to use it where `next/headers` does not exist, and a
388
+ * `session` store if you did not use the default cookie - the same two escape
389
+ * hatches `getConnectSession` takes, for the same reasons.
390
+ */
391
+ async function getConnectSessionToken(request, options = {}) {
392
+ const store = options.session ?? cookieSessionStore();
393
+ return (await store.read(request ?? (await requestFromHeaders()))) ?? null;
394
+ }
355
395
  /**
356
396
  * The signed-in user for a Server Component, Route Handler or Server Action,
357
397
  * or `null`.
@@ -179,4 +179,4 @@ class ConnectClient {
179
179
  }
180
180
  exports.ConnectClient = ConnectClient;
181
181
  /** Kept in step with `package.json` by `sdk.test.ts`. */
182
- exports.SDK_VERSION = "0.35.0";
182
+ exports.SDK_VERSION = "0.36.0";
@@ -54,6 +54,39 @@ class WorkspacesResource {
54
54
  json: input,
55
55
  });
56
56
  }
57
+ /**
58
+ * The billing identity of the workspace this session is on - its legal
59
+ * name, GSTIN, registered address and entity form.
60
+ *
61
+ * **For prefilling your own settings, once.** See
62
+ * `WorkspaceBillingProfile`, which carries the whole argument: this is the
63
+ * *Bill To* side, yours is the *Bill From* side, and they are the same facts
64
+ * for most customers without being the same field. There is no write form.
65
+ *
66
+ * ```ts
67
+ * // app/settings/page.tsx - offer "Use my billing details"
68
+ * const token = await getConnectSessionToken();
69
+ * const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
70
+ * ```
71
+ *
72
+ * **Needs two things, not one.** `workspaces:read` on your key, *and*
73
+ * `billing:read` on the signed-in member - a tax identity belongs to
74
+ * whoever can already see it in Connect's billing screens, so an ordinary
75
+ * member gets a 403 here where they get a roster from `members()`. That
76
+ * cannot be granted through a role on your application, deliberately.
77
+ *
78
+ * So **treat the 403 as a normal branch**, not an error: offer the prefill
79
+ * to whoever can use it and let everyone else type their own details. A
80
+ * workspace admin is the person who sets this up anyway.
81
+ */
82
+ async billingProfile(options) {
83
+ const result = await (0, http_js_1.request)(this.transport, {
84
+ method: "GET",
85
+ path: "/workspaces/billing-profile",
86
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
87
+ });
88
+ return result.billingProfile;
89
+ }
57
90
  /**
58
91
  * The team of the workspace this session is on.
59
92
  *
@@ -321,6 +321,45 @@ async function readSession(request, options) {
321
321
  const cachedAmbientSession = cache(async () => {
322
322
  return readSession(await requestFromHeaders(), {});
323
323
  });
324
+ /**
325
+ * The caller's opaque Connect **session token**, or `null` when signed out.
326
+ *
327
+ * ## What it is for
328
+ *
329
+ * Every `asUser` call on the server client needs one - `workspaces.*`,
330
+ * `scheduling.*`, the `/data` store's per-user form - and until this existed
331
+ * there was no way to obtain it inside a Server Component. `getConnectSession`
332
+ * answers *who* the caller is and deliberately does not hand back the
333
+ * credential, so a page that wanted to read its own workspace's roster had to
334
+ * either reimplement the cookie read or route the call through the browser.
335
+ *
336
+ * ```tsx
337
+ * // app/settings/page.tsx
338
+ * const token = await getConnectSessionToken();
339
+ * const members = token ? await connect.workspaces.members({ asUser: token }) : [];
340
+ * ```
341
+ *
342
+ * ## It is the cookie, unverified
343
+ *
344
+ * This reads the session store and nothing else - **no round trip, and no
345
+ * check that the token is still live.** A token from a revoked or expired
346
+ * session is a string that looks exactly like a good one, and Connect is what
347
+ * answers `401` for it. So this is not a sign-in check: use
348
+ * `getConnectSession()` for "is somebody here", and this for "hand Connect the
349
+ * credential they arrived with". A non-null answer means a cookie was present,
350
+ * not that it works.
351
+ *
352
+ * Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
353
+ * a `<Suspense>` boundary like any other request-scoped read.
354
+ *
355
+ * Pass a `Request` to use it where `next/headers` does not exist, and a
356
+ * `session` store if you did not use the default cookie - the same two escape
357
+ * hatches `getConnectSession` takes, for the same reasons.
358
+ */
359
+ export async function getConnectSessionToken(request, options = {}) {
360
+ const store = options.session ?? cookieSessionStore();
361
+ return (await store.read(request ?? (await requestFromHeaders()))) ?? null;
362
+ }
324
363
  /**
325
364
  * The signed-in user for a Server Component, Route Handler or Server Action,
326
365
  * or `null`.
@@ -175,4 +175,4 @@ export class ConnectClient {
175
175
  }
176
176
  }
177
177
  /** Kept in step with `package.json` by `sdk.test.ts`. */
178
- export const SDK_VERSION = "0.35.0";
178
+ export const SDK_VERSION = "0.36.0";
@@ -51,6 +51,39 @@ export class WorkspacesResource {
51
51
  json: input,
52
52
  });
53
53
  }
54
+ /**
55
+ * The billing identity of the workspace this session is on - its legal
56
+ * name, GSTIN, registered address and entity form.
57
+ *
58
+ * **For prefilling your own settings, once.** See
59
+ * `WorkspaceBillingProfile`, which carries the whole argument: this is the
60
+ * *Bill To* side, yours is the *Bill From* side, and they are the same facts
61
+ * for most customers without being the same field. There is no write form.
62
+ *
63
+ * ```ts
64
+ * // app/settings/page.tsx - offer "Use my billing details"
65
+ * const token = await getConnectSessionToken();
66
+ * const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
67
+ * ```
68
+ *
69
+ * **Needs two things, not one.** `workspaces:read` on your key, *and*
70
+ * `billing:read` on the signed-in member - a tax identity belongs to
71
+ * whoever can already see it in Connect's billing screens, so an ordinary
72
+ * member gets a 403 here where they get a roster from `members()`. That
73
+ * cannot be granted through a role on your application, deliberately.
74
+ *
75
+ * So **treat the 403 as a normal branch**, not an error: offer the prefill
76
+ * to whoever can use it and let everyone else type their own details. A
77
+ * workspace admin is the person who sets this up anyway.
78
+ */
79
+ async billingProfile(options) {
80
+ const result = await request(this.transport, {
81
+ method: "GET",
82
+ path: "/workspaces/billing-profile",
83
+ headers: actingAs(options.asUser),
84
+ });
85
+ return result.billingProfile;
86
+ }
54
87
  /**
55
88
  * The team of the workspace this session is on.
56
89
  *
@@ -120,6 +120,44 @@ declare function handle(request: Request): Promise<Response>;
120
120
  * when you need to configure one of them.
121
121
  */
122
122
  export { handle as GET, handle as POST };
123
+ /**
124
+ * The caller's opaque Connect **session token**, or `null` when signed out.
125
+ *
126
+ * ## What it is for
127
+ *
128
+ * Every `asUser` call on the server client needs one - `workspaces.*`,
129
+ * `scheduling.*`, the `/data` store's per-user form - and until this existed
130
+ * there was no way to obtain it inside a Server Component. `getConnectSession`
131
+ * answers *who* the caller is and deliberately does not hand back the
132
+ * credential, so a page that wanted to read its own workspace's roster had to
133
+ * either reimplement the cookie read or route the call through the browser.
134
+ *
135
+ * ```tsx
136
+ * // app/settings/page.tsx
137
+ * const token = await getConnectSessionToken();
138
+ * const members = token ? await connect.workspaces.members({ asUser: token }) : [];
139
+ * ```
140
+ *
141
+ * ## It is the cookie, unverified
142
+ *
143
+ * This reads the session store and nothing else - **no round trip, and no
144
+ * check that the token is still live.** A token from a revoked or expired
145
+ * session is a string that looks exactly like a good one, and Connect is what
146
+ * answers `401` for it. So this is not a sign-in check: use
147
+ * `getConnectSession()` for "is somebody here", and this for "hand Connect the
148
+ * credential they arrived with". A non-null answer means a cookie was present,
149
+ * not that it works.
150
+ *
151
+ * Reads `cookies()` transitively, so under `cacheComponents` it belongs inside
152
+ * a `<Suspense>` boundary like any other request-scoped read.
153
+ *
154
+ * Pass a `Request` to use it where `next/headers` does not exist, and a
155
+ * `session` store if you did not use the default cookie - the same two escape
156
+ * hatches `getConnectSession` takes, for the same reasons.
157
+ */
158
+ export declare function getConnectSessionToken(request?: Request, options?: {
159
+ session?: ConnectSessionStore;
160
+ }): Promise<string | null>;
123
161
  /**
124
162
  * The signed-in user for a Server Component, Route Handler or Server Action,
125
163
  * or `null`.
@@ -138,4 +138,4 @@ export declare class ConnectClient {
138
138
  constructor(options: ConnectClientOptions);
139
139
  }
140
140
  /** Kept in step with `package.json` by `sdk.test.ts`. */
141
- export declare const SDK_VERSION = "0.35.0";
141
+ export declare const SDK_VERSION = "0.36.0";
@@ -42,7 +42,12 @@ export interface RescheduleBookingInput extends ActingAsOption {
42
42
  export interface ListBookingsInput extends ActingAsOption {
43
43
  workspaceId: string;
44
44
  status?: BookingStatus;
45
- /** A Connect workspace member id - only bookings that committed them. */
45
+ /**
46
+ * A `ConnectWorkspaceMember.memberId` - only bookings that committed them.
47
+ *
48
+ * **Not a `userId`.** Pass one and the filter matches nothing, which comes
49
+ * back as an empty diary rather than an error.
50
+ */
46
51
  hostMemberId?: string;
47
52
  from?: string | Date;
48
53
  to?: string | Date;
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { ConnectInvitation, ConnectMemberAccess, ConnectMemberAccessResult, ConnectWorkspaceMember, UserWorkspace, WorkspaceSwitchResult } from "../../types.js";
2
+ import type { ConnectInvitation, ConnectMemberAccess, ConnectMemberAccessResult, ConnectWorkspaceMember, UserWorkspace, WorkspaceBillingProfile, WorkspaceSwitchResult } from "../../types.js";
3
3
  /**
4
4
  * Which workspaces the signed-in user belongs to, moving between them, who
5
5
  * else is on the team, and inviting or granting access to your application.
@@ -41,6 +41,34 @@ export declare class WorkspacesResource {
41
41
  }, options: {
42
42
  asUser: string;
43
43
  }): Promise<WorkspaceSwitchResult>;
44
+ /**
45
+ * The billing identity of the workspace this session is on - its legal
46
+ * name, GSTIN, registered address and entity form.
47
+ *
48
+ * **For prefilling your own settings, once.** See
49
+ * `WorkspaceBillingProfile`, which carries the whole argument: this is the
50
+ * *Bill To* side, yours is the *Bill From* side, and they are the same facts
51
+ * for most customers without being the same field. There is no write form.
52
+ *
53
+ * ```ts
54
+ * // app/settings/page.tsx - offer "Use my billing details"
55
+ * const token = await getConnectSessionToken();
56
+ * const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;
57
+ * ```
58
+ *
59
+ * **Needs two things, not one.** `workspaces:read` on your key, *and*
60
+ * `billing:read` on the signed-in member - a tax identity belongs to
61
+ * whoever can already see it in Connect's billing screens, so an ordinary
62
+ * member gets a 403 here where they get a roster from `members()`. That
63
+ * cannot be granted through a role on your application, deliberately.
64
+ *
65
+ * So **treat the 403 as a normal branch**, not an error: offer the prefill
66
+ * to whoever can use it and let everyone else type their own details. A
67
+ * workspace admin is the person who sets this up anyway.
68
+ */
69
+ billingProfile(options: {
70
+ asUser: string;
71
+ }): Promise<WorkspaceBillingProfile>;
44
72
  /**
45
73
  * The team of the workspace this session is on.
46
74
  *
@@ -1741,6 +1741,59 @@ export interface UserWorkspace {
1741
1741
  /** Whether it holds an entitling subscription to your application. */
1742
1742
  subscribed: boolean;
1743
1743
  }
1744
+ /**
1745
+ * A workspace's registered address, as its billing profile reports it.
1746
+ *
1747
+ * `stateCode` is the Indian state code, and it is the field that matters most
1748
+ * of the six: it decides CGST+SGST versus IGST on anything you raise against
1749
+ * this entity.
1750
+ */
1751
+ export interface WorkspaceBillingAddress {
1752
+ line1: string | null;
1753
+ line2: string | null;
1754
+ city: string | null;
1755
+ stateCode: string | null;
1756
+ postalCode: string | null;
1757
+ countryCode: string;
1758
+ }
1759
+ /**
1760
+ * A workspace's **self-declared** billing identity - what the customer gave
1761
+ * CosmoOps so we can invoice them.
1762
+ *
1763
+ * ## Read it to prefill, not to share
1764
+ *
1765
+ * This is the *Bill To* side: who we invoice for the subscription. Your own
1766
+ * application's letterhead is the *Bill From* side. For a single-entity
1767
+ * customer they are the same three facts, which is the whole reason this is
1768
+ * readable - a GSTIN typed twice is a GSTIN that diverges, and the copy on a
1769
+ * quotation is the one that decides how the tax splits.
1770
+ *
1771
+ * They are **not the same field**, though. A group that bills us through a
1772
+ * holding company while quoting under a trading subsidiary has to stay
1773
+ * expressible, so copy this into your own settings on first setup and keep
1774
+ * your copy authoritative. There is deliberately no write form: an
1775
+ * application must not be able to edit the details its own invoices are
1776
+ * raised against.
1777
+ *
1778
+ * ## Nothing here is verified
1779
+ *
1780
+ * It is what the customer typed, not a KYB record - no PAN check stands
1781
+ * behind it. A convenience, never evidence of identity.
1782
+ *
1783
+ * Every field is nullable, because every one is optional to a customer who has
1784
+ * not needed an invoice yet. Fall back to the workspace name you already hold
1785
+ * on the session rather than printing an empty letterhead.
1786
+ */
1787
+ export interface WorkspaceBillingProfile {
1788
+ /** The registered entity name, or `null` - most workspaces never set one. */
1789
+ legalName: string | null;
1790
+ /** GSTIN, 15 characters. `null` for an unregistered customer. */
1791
+ taxId: string | null;
1792
+ /** `null` as a whole when none was given - one check, not six. */
1793
+ billingAddress: WorkspaceBillingAddress | null;
1794
+ /** The legal entity form, self-declared. `null` for an individual workspace. */
1795
+ businessType: string | null;
1796
+ }
1744
1797
  /**
1745
1798
  * The result of a workspace switch.
1746
1799
  *
@@ -1761,6 +1814,20 @@ export interface WorkspaceSwitchResult {
1761
1814
  * catalog, not the member's Connect roles.
1762
1815
  */
1763
1816
  export interface ConnectWorkspaceMember {
1817
+ /**
1818
+ * The id the **calendar** speaks in - `Booking.hostMemberIds` holds these,
1819
+ * and so does `listBookings`' `hostMemberId` filter.
1820
+ *
1821
+ * Carried beside `userId` because the two are different identifiers and
1822
+ * scheduling is the one surface that uses this one: joining a diary to a
1823
+ * roster, filtering it to one host, or putting a name against an
1824
+ * appointment all go through `memberId`. Everywhere else a member is a
1825
+ * `userId`.
1826
+ *
1827
+ * Opaque and workspace-scoped. Do not derive it from `userId` or address a
1828
+ * member by it elsewhere in this API.
1829
+ */
1830
+ memberId: string;
1764
1831
  userId: string;
1765
1832
  email: string;
1766
1833
  displayName: string | null;
@@ -1885,9 +1952,15 @@ export interface Booking {
1885
1952
  /**
1886
1953
  * The workspace members committed to this appointment.
1887
1954
  *
1888
- * These are `WorkspaceMember.id` values - the same ids `workspaces.listMembers`
1889
- * returns and `listBookings`' `hostMemberId` filter takes, so an owner column
1890
- * is a lookup against a roster you already have rather than another round trip.
1955
+ * These are `ConnectWorkspaceMember.memberId` values - what
1956
+ * `workspaces.members()` returns as `memberId` and what `listBookings`'
1957
+ * `hostMemberId` filter takes, so a host column is a lookup against a roster
1958
+ * you already have rather than another round trip.
1959
+ *
1960
+ * **Not `userId`.** An earlier version of this comment said these were the
1961
+ * ids `workspaces.listMembers` returns, which was wrong twice over: that
1962
+ * method is `workspaces.members()`, and until `memberId` was added to its
1963
+ * rows it returned no id these could be matched against at all.
1891
1964
  *
1892
1965
  * Resolved when the slot was held and frozen there: a round-robin event type
1893
1966
  * picks one host, a collective one commits the whole group, and re-pointing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
@@ -79,7 +79,7 @@
79
79
  "clean": "rm -rf dist",
80
80
  "build": "bun run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.types.json && bun run scripts/finalize-dist.ts",
81
81
  "verify:pack": "bun run scripts/verify-pack.ts",
82
- "prepublishOnly": "bun run build && bun run verify:pack",
82
+ "prepublishOnly": "bun run build && bun run verify:pack && bun test",
83
83
  "test": "bun test",
84
84
  "docs:md": "typedoc --options typedoc.markdown.json",
85
85
  "storybook": "storybook dev -p 6106",