@fusebase/fusebase-gate-sdk 2.3.34-sdk.9 → 2.5.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.
@@ -0,0 +1,1054 @@
1
+ # Release Notes 2.3.34-sdk.10
2
+
3
+ - Current ref: `HEAD`
4
+ - Previous tag: `v2.2.15`
5
+ - Generated at: 2026-07-27T04:36:05.229Z
6
+
7
+ ## Included Drafts
8
+
9
+ - `docs/release-notes/2026-05-20-app-magic-link-redirectpath-fix.md` - 2026-05-20-app-magic-link-redirectpath-fix
10
+ - `docs/release-notes/2026-05-27-visitor-api-edge-skills.md` - 2026-05-27 — Visitor access vs open `/api/*` (skill guidance)
11
+ - `docs/release-notes/2026-06-05-isolated-store-secret-guidance.md` - Isolated store secret guidance
12
+ - `docs/release-notes/2026-06-08-get-org-url.md` - Get organization URL
13
+ - `docs/release-notes/2026-06-15-create-portal-folder.md` - createPortalFolder
14
+ - `docs/release-notes/2026-06-15-duplicate-portal-item.md` - duplicatePortalItem
15
+ - `docs/release-notes/2026-06-15-update-portal-access.md` - updatePortalAccess
16
+ - `docs/release-notes/2026-06-16-bulk-invite-to-portal.md` - Bulk invite users to a portal
17
+ - `docs/release-notes/2026-06-16-getme-timezone-time-format.md` - getMe returns user timezone and time-format preferences
18
+ - `docs/release-notes/2026-06-16-token-rls-bypass-permission-validation.md` - Token creation accepts isolated_store RLS break-glass permissions
19
+ - `docs/release-notes/2026-06-19-token-provisioning-pagination.md` - Summary
20
+ - `docs/release-notes/2026-06-20-app-portal-embeds.md` - List an app's portal embeds
21
+ - `docs/release-notes/2026-06-26-publish-portal-draft.md` - publishPortalDraft
22
+ - `docs/release-notes/2026-06-29-app-portal-embeds-iframe-kind.md` - App portal embeds — detect iframe embeds, add `kind`
23
+ - `docs/release-notes/2026-06-30-append-workspace-note-content.md` - Append workspace note content
24
+ - `docs/release-notes/2026-06-30-client-app-invite.md` - Client → client app invite op (NIM-40541 / NIM-41954)
25
+ - `docs/release-notes/2026-06-30-client-portal-invite.md` - Client → client portal invite authz (NIM-40541 / NIM-41950)
26
+ - `docs/release-notes/2026-07-02-markdown-notes-crud.md` - Markdown (v3) notes CRUD
27
+ - `docs/release-notes/2026-07-02-member-removal-client-deletion.md` - Member removal and client deletion scheduling
28
+ - `docs/release-notes/2026-07-03-auto-confirm-email-registration.md` - 2026-07-03-auto-confirm-email-registration
29
+ - `docs/release-notes/2026-07-04-bulk-app-magic-links.md` - Bulk app magic-link invites
30
+ - `docs/release-notes/2026-07-09-workspace-portal-member-read.md` - Workspace and portal member read
31
+ - `docs/release-notes/2026-07-13-getme-no-health-read.md` - getMe no longer requires health.read
32
+ - `docs/release-notes/2026-07-23-add-portal-blank-note-block.md` - Add Portal Blank Note Block
33
+ - `docs/release-notes/2026-07-23-conditional-remove-org-member.md` - Conditional removeOrgMember (optional preconditions)
34
+ - `docs/release-notes/2026-07-24-add-portal-chat-block.md` - Add portal Chat block + channel discovery
35
+ - `docs/release-notes/2026-07-24-add-portal-embed-block.md` - Add Portal Embed Block
36
+ - `docs/release-notes/2026-07-24-create-portal-page-with-note-portal-service.md` - createPortalPageWithNote moves into portal-service
37
+ - `docs/release-notes/2026-07-25-list-portal-chat-users.md` - List portal Chat DM target users
38
+
39
+ ## Summary
40
+
41
+ ### 2026-05-20-app-magic-link-redirectpath-fix
42
+
43
+ Bug fix for the AI App **Magic Link** ops. The Gate controller coerced an
44
+ omitted `redirectPath` to an explicit `null` before forwarding the request to
45
+ `nimbus-ai`. nimbus-ai accepts an _absent_ `redirectPath` but rejects an
46
+ explicit `null` (`"must be string"`), so every `createAppMagicLink` /
47
+ `requestAppMagicLink` call that did not include a deep-link path failed with
48
+ HTTP 400. That broke the two most common magic-link scenarios — inviting a
49
+ client so they land on the app root, and visitor self-service where the
50
+ visitor only types an email.
51
+
52
+ ### 2026-05-27 — Visitor access vs open `/api/*` (skill guidance)
53
+
54
+ Clarify in MCP prompts that `--access=visitor` enables guest access to the App host via platform `/_auth/` and `fbsfeaturetoken`, not unauthenticated `/api/*`. Reduces false "broken API" reports from curl and agent smoke tests.
55
+
56
+ ### Isolated store secret guidance
57
+
58
+ Clarified Gate isolated-store guidance so app agents treat store identity as a Gate-resolved platform resource, not app secret or runtime environment configuration.
59
+
60
+ ### Get organization URL
61
+
62
+ Gate exposes `GET /:orgId/url` (`getOrgUrl`) to resolve the canonical HTTPS base URL for an organization. The hostname follows org-service rules: custom CNAME domain when configured, otherwise `{sub}.{FUSEBASE_HOST}`.
63
+
64
+ ### createPortalFolder
65
+
66
+ Adds the `createPortalFolder` Gate operation (NIM-41312): create a new empty
67
+ root folder in a portal from CLI/SDK/MCP agents. The folder is staged in the
68
+ portal's customizer draft (via the NIM-41542 changeEvents/draft mechanism), not
69
+ published immediately — the portal owner reviews and publishes it in the
70
+ customizer.
71
+
72
+ ### duplicatePortalItem
73
+
74
+ Adds the `duplicatePortalItem` Gate operation (NIM-41316): copy a folder or page
75
+ (and, for a folder, its whole subtree of copyable children) inside the same
76
+ portal from CLI/SDK/MCP agents. The copy is staged in the portal's customizer
77
+ draft (via the NIM-41542 changeEvents/draft mechanism), not published
78
+ immediately — the portal owner reviews and publishes it in the customizer.
79
+
80
+ ### updatePortalAccess
81
+
82
+ Adds the `updatePortalAccess` Gate operation (NIM-41308): set a portal's access
83
+ mode from CLI/SDK/MCP agents. The change is staged in the portal's customizer
84
+ draft (via the NIM-41542 changeEvents/draft mechanism), not published
85
+ immediately — the portal owner reviews and publishes it in the customizer.
86
+
87
+ ### Bulk invite users to a portal
88
+
89
+ Gate exposes `POST /:orgId/portals/:portalId/invite/bulk` (`bulkInviteToPortal`) to invite many users to a single portal in one request, replacing N sequential `inviteToPortal` calls. Invites run with bounded concurrency (default 5, max 5); the rest are queued. Portal discovery (`listPortals`/`getPortal`) runs once for the whole batch. An optional `background` mode returns immediately without blocking the caller.
90
+
91
+ ### getMe returns user timezone and time-format preferences
92
+
93
+ Extend the `GET /me` (`getMe`) response with a `preferences` object so Fusebase Apps can read the user's timezone and clock preference (12h/24h) without a separate call.
94
+
95
+ ### Token creation accepts isolated_store RLS break-glass permissions
96
+
97
+ Fix token creation rejecting `isolated_store.rls.bypass` and `isolated_store.rls.delegate` with `Invalid permission` even though they are part of the Gate permission catalog.
98
+
99
+ ### List an app's portal embeds
100
+
101
+ Adds a Gate operation that returns the portal pages where a given app is embedded
102
+ (as an `app-feature` brick), scoped to the org. Backs the app card "Resources"
103
+ popup, the in-app runtime, and the public-api/CLI proxy.
104
+
105
+ ### publishPortalDraft
106
+
107
+ Adds the `publishPortalDraft` Gate operation (NIM-41873): publish a portal's
108
+ staged customizer draft to the live portal from CLI/SDK/MCP agents — the
109
+ programmatic equivalent of the customizer's Publish button. This lets an agent
110
+ make its previously staged content/settings ops (folders, pages, blocks, access
111
+ mode, custom code) visible to clients without opening the customizer.
112
+
113
+ ### App portal embeds — detect iframe embeds, add `kind`
114
+
115
+ Extends `listAppPortalEmbeds` to also report portal pages where the app is embedded
116
+ via a regular `embed` (iframe) block whose `contentUrl` points at the app host, in
117
+ addition to the existing `app-feature` brick embeds. Every entry now carries a
118
+ `kind` discriminator. Detection works for all consumers, including the in-app
119
+ runtime app-token path.
120
+
121
+ ### Append workspace note content
122
+
123
+ Add an append-only workspace note content operation for apps that need to add content to an existing note without replacing the note body.
124
+
125
+ ### Client → client app invite op (NIM-40541 / NIM-41954)
126
+
127
+ New op `createAppClientInviteMagicLink`
128
+ (`POST /:orgId/apps/:appId/client-invites`): an authenticated **client** that
129
+ already has access to an AI App can invite another client. The caller's
130
+ `orgRole` must be `client` (owners/admins use the separate `createAppMagicLink`
131
+ flow); the op is gated by the `FEATURE_FLAGS=client_invite` env kill-switch
132
+ (fail-closed). The invitee is always provisioned as a `client` and inherits the
133
+ inviter's app scope (a user principal on every feature) — no role/access
134
+ selectors. The owner/admin `createAppMagicLink` (`app_magic_link.write`) flow is
135
+ unchanged.
136
+
137
+ ### Client → client portal invite authz (NIM-40541 / NIM-41950)
138
+
139
+ `inviteToPortal` / `bulkInviteToPortal` now allow a caller whose `orgRole` is
140
+ `client` to invite other clients, gated by the portal's `allowClientsToInvite`
141
+ setting and the `FEATURE_FLAGS=client_invite` env kill-switch (fail-closed:
142
+ off → client callers are denied `portal_client_invite_disabled`).
143
+ Members/managers/owners are unaffected.
144
+
145
+ ### Markdown (v3) notes CRUD
146
+
147
+ Add Gate contracts, controllers, and MCP-visible operations for v3 markdown notes: create, read, replace, and append markdown stored as the source of truth in note-service (no editor-service involved). Part of Notes v3 Iteration 1 (NIM-42034, NIM-42042).
148
+
149
+ ### Member removal and client deletion scheduling
150
+
151
+ Gate now exposes member-removal operations for organization, workspace, and portal membership using app-visible user/workspace ids, plus delayed Client account deletion scheduling.
152
+
153
+ ### Bulk app magic-link invites
154
+
155
+ Gate exposes `POST /:orgId/apps/:appId/magic-links/bulk` (`bulkCreateAppMagicLinks`) to invite many users to a single app in one request, replacing N sequential `createAppMagicLink` calls. Invites run with bounded concurrency (default 5, max 5); the rest are queued in-process. An optional `background` mode returns immediately without blocking the caller. Mirrors the portal `bulkInviteToPortal` pattern (NIM-41651).
156
+
157
+ ### Workspace and portal member read
158
+
159
+ Gate now exposes read operations that list the members of a single workspace or portal scoped to an organization, closing the gap where only member writes and the org-wide roster were available.
160
+
161
+ ### getMe no longer requires health.read
162
+
163
+ `GET /me` (`getMe`) no longer declares a required Gate permission. Any authenticated **user** or **token** subject may call it with a valid session, without `health.read` in the grant.
164
+
165
+ ### Add Portal Blank Note Block
166
+
167
+ New operation `addPortalBlankNoteBlock`: creates an empty Fusebase note, shares it
168
+ into the portal and stages one `addBlock` event on an existing portal page — the
169
+ programmatic equivalent of the customizer's "Blank Block" action. No page and no
170
+ sidebar menu item is created.
171
+
172
+ ### Conditional removeOrgMember (optional preconditions)
173
+
174
+ `DELETE /{orgId}/users/{userId}` (`removeOrgMember`) accepts two optional query preconditions. Without them the call stays exactly as unconditional as before.
175
+
176
+ - `expectedRole` — remove only while the member still holds that org role.
177
+ - `expectedJoinedAfter` — unix seconds; remove only when the membership was created at or after that moment.
178
+
179
+ Either mismatch returns **409** and removes nothing, with `data.errorCode` = `org_member_role_mismatch` or `org_member_joined_before_expected`.
180
+
181
+ ### Add portal Chat block + channel discovery
182
+
183
+ Gate exposes two new portal operations that stage a Chat widget on a portal page,
184
+ mirroring the customizer's "Chat" action:
185
+
186
+ - `GET /:orgId/portals/:portalId/chat/channels` (`listPortalChatChannels`) — the
187
+ portal's real, listable chat channels (DMs, group DMs and hidden channels
188
+ excluded; default channel first) for the Chat block picker.
189
+ - `POST /:orgId/portals/:portalId/pages/:pageId/blocks/chat` (`addPortalChatBlock`)
190
+ — stages one Chat block whose `target` is a discriminated union: a channel
191
+ (`target: { type: 'channel', channelId }`) or a DM (`target: { type: 'dm', userId }`).
192
+ One active Chat block per page.
193
+
194
+ Gate stays thin: all channel/DM revalidation, the chat widget configuration and the
195
+ draft logic live in portal-service.
196
+
197
+ ### Add Portal Embed Block
198
+
199
+ New operation `addPortalEmbedBlock`: stages one Customizer-compatible Embed block
200
+ ("Embeds & Integrations") on an existing portal page — the programmatic equivalent
201
+ of the customizer's Embed action. No page and no sidebar menu item is created, and
202
+ nothing is published automatically.
203
+
204
+ As part of the same change, the generic `addPortalBlock` **no longer accepts
205
+ `type: "embed"`** — Embed blocks now go exclusively through `addPortalEmbedBlock`.
206
+
207
+ ### createPortalPageWithNote moves into portal-service
208
+
209
+ `createPortalPageWithNote` now delegates the whole use case to a new portal-service
210
+ endpoint (`POST /portals/:portalId/pages/note`), the same way `addPortalBlankNoteBlock`
211
+ does. Gate keeps the public contract, auth/authz and error mapping; note creation,
212
+ sharing, optional initial content, slug/URL/index calculation, the canonical
213
+ addPage/addMenuItem/addBlock events and the draft append + conflict retry all live in
214
+ portal-service. The public operation, HTTP path, request/response shape, permission
215
+ (`portals.write`) and MCP semantics are unchanged.
216
+
217
+ ### List portal Chat DM target users
218
+
219
+ Gate exposes a new portal operation that searches a portal org's active members
220
+ and owner as DM targets for the Chat block picker, completing the Chat block flow
221
+ (`listPortalChatChannels` + `addPortalChatBlock` shipped earlier):
222
+
223
+ - `GET /:orgId/portals/:portalId/chat/users?query=&limit=&cursor=`
224
+ (`listPortalChatUsers`) — a single bounded page of
225
+ `{ id, displayName, email, username? }` plus the matching `total` and an opaque
226
+ `nextCursor`. `query` is optional: omit it to browse the first page when the
227
+ person is not known. Call it before `addPortalChatBlock` with a
228
+ `{ type: 'dm', userId }` target; pass the exact `users[].id`.
229
+
230
+ Gate stays thin: the name match, member scoping and opaque cursor all live in
231
+ org-service (`/orgs/:org/members/search`); portal-service forwards the caller's
232
+ user context and the query/limit/cursor.
233
+
234
+
235
+ ## API / SDK Changes
236
+
237
+ ### 2026-05-20-app-magic-link-redirectpath-fix
238
+
239
+ - No contract, schema, OpenAPI, SDK, or permission change. The Gate HTTP
240
+ surface is identical; `redirectPath` remains optional in both request
241
+ contracts.
242
+ - `AppMagicLinksController` (`src/controllers/app-magic-links/app-magic-links.ts`)
243
+ now forwards `redirectPath` to nimbus-ai **only when the caller provided
244
+ one** (`...(body.redirectPath != null ? { redirectPath } : {})`), for both
245
+ `createAppMagicLink` and `requestAppMagicLink`. An empty-string
246
+ `redirectPath` is still forwarded verbatim.
247
+
248
+ ### Isolated store secret guidance
249
+
250
+ No API or SDK contract changes.
251
+
252
+ MCP prompts and generated skill references now explicitly say not to register `storeId`, database IDs, physical database names, or provider connection details through `fusebase secret create`.
253
+
254
+ ### Get organization URL
255
+
256
+ - Added `getOrgUrl` operation and `OrgsApi.getOrgUrl` SDK client method.
257
+ - Response fields: `url`, `host`, `kind` (`cname` | `subdomain`), `sub`, `customDomain`, `domainShorter`.
258
+ - Permission: `org.read` with org-scoped authz.
259
+
260
+ ### createPortalFolder
261
+
262
+ - New op `createPortalFolder` — `POST /:orgId/portals/:portalId/folders`.
263
+ - Body: `name` (required), `icon` (optional, default `folder`).
264
+ - Response: `{ folderId, pageId, url, branchId, seqs, staged: true }`.
265
+ - Permission: `portals.write`.
266
+ - New contract types `CreatePortalFolderRequest` / `CreatePortalFolderResponse`
267
+ (regenerated `runtime-schema-defs`).
268
+ - MCP `portals` prompt bumped to 1.8.0 with folder-creation guidance.
269
+
270
+ ### duplicatePortalItem
271
+
272
+ - New op `duplicatePortalItem` —
273
+ `POST /:orgId/portals/:portalId/items/:itemId/duplicate`.
274
+ - Body: `name` (optional, default `<source name> copy`), `parentId` (optional,
275
+ default: next to the source).
276
+ - Response: `{ itemId, pageId?, url, itemCount, branchId, seqs, staged: true }`.
277
+ - Permission: `portals.write`.
278
+ - New contract types `DuplicatePortalItemRequest` / `DuplicatePortalItemResponse`
279
+ (regenerated `runtime-schema-defs`).
280
+ - MCP `portals` prompt bumped to 1.10.0 with duplication guidance.
281
+
282
+ ### updatePortalAccess
283
+
284
+ - New op `updatePortalAccess` — `POST /:orgId/portals/:portalId/access`.
285
+ - Body: `accessMode` ∈ `open` | `invite-only` | `email` | `email-verified`.
286
+ - Response: `{ accessMode, branchId, seq, staged: true }`.
287
+ - Permission: `portals.write`.
288
+ - New contract types `UpdatePortalAccessRequest` / `UpdatePortalAccessResponse`
289
+ (regenerated `runtime-schema-defs`).
290
+ - MCP `portals` prompt bumped to 1.7.0 with access-mode guidance.
291
+
292
+ ### Bulk invite users to a portal
293
+
294
+ - Added `bulkInviteToPortal` operation and `PortalsApi.bulkInviteToPortal` SDK client method.
295
+ - Request: `invitations[]` (each item matches `inviteToPortal`: `email` required; optional `fullName`, `orgRole`, `workspaceRole`, `isFullAccess`), optional `concurrency` (1..5, default 5), optional `background` (default false).
296
+ - Response: `total`, `status` (`completed` | `processing`). For `completed`: `succeeded`, `failed`, `results[]` (`{ email, status, magicLink?, url?, userId?, error? }`). For `processing`: `accepted`.
297
+ - Per-invitation `orgRole`/`isFullAccess` semantics are identical to `inviteToPortal` (shared `invitePortalUser` helper).
298
+ - Permission: `portals.manage` with org-scoped authz (same as `inviteToPortal`).
299
+ - MCP: `bulkInviteToPortal` is MCP-visible under the `portals` prompt group; the prompt now documents batching, concurrency, background mode, and the client access-clarification rule.
300
+
301
+ ### getMe returns user timezone and time-format preferences
302
+
303
+ - `MeResponse` gains a nullable `preferences` object (`MePreferences` schema):
304
+ - `timezone` — IANA timezone name, e.g. `Europe/Moscow` (nullable)
305
+ - `timezoneOffset` — current UTC offset string, e.g. `+03:00`, recomputed from the timezone at request time so it stays correct across DST (nullable)
306
+ - `timeFormat` — `12h` or `24h`, derived from the user's `dateTimeLocale`
307
+ - `dateTimeLocale` — raw display locale, e.g. `en-US` (12h) / `en-GB` (24h) (nullable)
308
+ - `preferences` is `null` when the auth context is not user-bound (e.g. an app token without a user).
309
+
310
+ ### Token creation accepts isolated_store RLS break-glass permissions
311
+
312
+ - No API contract changes.
313
+ - Token `POST` now accepts all permissions from the Gate catalog, including `isolated_store.rls.bypass` and `isolated_store.rls.delegate`.
314
+
315
+ ### List an app's portal embeds
316
+
317
+ - New operation `listAppPortalEmbeds`: `GET /:orgId/apps/:appId/portal-embeds`.
318
+ - Auth: `userOrToken('portals.read', PATH)` — internal user or app token.
319
+ - Response `ListAppPortalEmbedsResponse`: `{ portalEmbeds: [{ portal { globalId, name }, page { globalId, title }, url }] }`, one entry per page (deduped).
320
+ - Lives in the new `app-embed-targets` controller/tag (a bucket for app embed-target lookups; `portal-embeds` is the first kind).
321
+ - MCP: not exposed (`mcp.enabled: false`).
322
+ - Downstream: calls portal-service `GET /blocks/search` with the app-embed filter (`org`, `blockType=flexible`, `brickType=app-feature`, `brickMatch=featureId:<appId>`).
323
+
324
+ ### publishPortalDraft
325
+
326
+ - New op `publishPortalDraft` — `POST /:orgId/portals/:portalId/publish`.
327
+ - No body.
328
+ - Response: `{ branchId, newBranchId, appliedEventCount, lastPublishedAt, published: true }`.
329
+ - Permission: `portals.write`.
330
+ - New contract type `PublishPortalDraftResponse` (regenerated `runtime-schema-defs`).
331
+ - MCP `portals` prompt bumped to 1.15.0 with publish guidance.
332
+
333
+ ### App portal embeds — detect iframe embeds, add `kind`
334
+
335
+ - `ListAppPortalEmbedsResponse` entries gain `kind: 'app-feature' | 'iframe'`
336
+ (additive, non-breaking).
337
+ - `listAppPortalEmbeds` now merges two portal-service searches:
338
+ - existing brick search (`blockType=flexible`, `brickType=app-feature`,
339
+ `brickMatch=featureId:<appId>`) → `kind: 'app-feature'`.
340
+ - new iframe search (`blockType=embed`, `attrContains=contentUrl:<host>` per
341
+ resolved host) → `kind: 'iframe'`.
342
+ - Host(s) resolved via nimbus-ai `apiGetAppByGlobalId` using a secret-only
343
+ ("visitor") header (no caller user id), so the app-token runtime path works too.
344
+ Takes the host of `url` and, when set, of `customDomainFqdn` (deduped).
345
+ - Merge is per page with brick precedence: a page matched by both stays
346
+ `app-feature`.
347
+ - Graceful degrade: if host resolution yields nothing (app not found / unpublished
348
+ / nimbus-ai unreachable), the iframe search is skipped and brick embeds are
349
+ returned normally — the op never fails on this.
350
+
351
+ ### Append workspace note content
352
+
353
+ - Added `appendWorkspaceNoteContent`.
354
+ - The operation accepts a workspace note id plus non-empty `content`, with optional `format: "text" | "html"`.
355
+ - The operation appends to the end of the note and returns the refreshed note metadata plus `note.md`.
356
+ - Regenerated SDK/OpenAPI artifacts include the new Notes API method and request contract.
357
+
358
+ ### Client → client app invite op (NIM-40541 / NIM-41954)
359
+
360
+ - New op + request schema `CreateAppClientInviteRequest` (`email`,
361
+ `redirectPath?`); response reuses `CreateAppMagicLinkResponse`. SDK + OpenAPI
362
+ regenerated.
363
+ - New permission `app_magic_link.client_invite`, granted to clients (and, via
364
+ the full set, to members/managers/owners). Distinct from
365
+ `app_magic_link.write` so the owner/admin flow stays owner-only.
366
+ - `403` when the caller is not a client, the feature flag is off, the caller has
367
+ no app-invite permission, or the inviter has no access to the app
368
+ (`errorCode: forbidden` for the downstream no-access case).
369
+
370
+ ### Client → client portal invite authz (NIM-40541 / NIM-41950)
371
+
372
+ Request/response schemas and op-level authz are unchanged; enforcement is
373
+ internal to the controller. New error codes are returned in the existing error
374
+ response body under `apiData.errorCode` and are now documented as `403`
375
+ responses on both ops in the OpenAPI spec:
376
+
377
+ - `portal_client_invite_disabled` (403) — portal setting off or feature flag off.
378
+ - `forbidden` (403) — caller has no access to the portal.
379
+ - `client_limit_reached` (403) — org client seat limit would be exceeded.
380
+
381
+ ### Markdown (v3) notes CRUD
382
+
383
+ - Added `createWorkspaceMarkdownNote` (`POST /:orgId/workspaces/:workspaceId/markdown-notes`).
384
+ - Added `getWorkspaceMarkdownNote` (`GET .../markdown-notes/:noteId`) returning the markdown source of truth plus `revision`.
385
+ - Added `updateWorkspaceMarkdownNoteContent` (`PUT .../markdown-notes/:noteId/content`) replacing the full markdown document with a required optimistic `revision` lock.
386
+ - Added `appendWorkspaceMarkdownNoteContent` (`POST .../markdown-notes/:noteId/content/append`) with an optional `revision` lock.
387
+ - Stale-revision writes return HTTP 409 with `data.errorCode = markdown_note_revision_conflict` and `data.currentRevision`.
388
+ - Regenerated SDK/OpenAPI artifacts include the new Notes API methods and contracts.
389
+
390
+ ### Member removal and client deletion scheduling
391
+
392
+ - Added `removeOrgMember`.
393
+ - Added `removeWorkspaceMember`.
394
+ - Added `removePortalMember`.
395
+ - Added `scheduleClientAccountDeletion`, which schedules delayed deletion only for Client-role org members.
396
+
397
+ ### Bulk app magic-link invites
398
+
399
+ - Added `bulkCreateAppMagicLinks` operation and `AppMagicLinksApi.bulkCreateAppMagicLinks` SDK client method.
400
+ - Request: `invitations[]` (each item matches `createAppMagicLink`: `email` required; optional `redirectPath`, `addToAccessPrincipals`), optional `concurrency` (1..5, default 5), optional `background` (default false).
401
+ - Response: `total`, `status` (`completed` | `processing`). For `completed`: `succeeded`, `failed`, `results[]` (`{ email, status, id?, magicLinkUrl?, expiresAt?, error? }`, request order). For `processing`: `accepted`.
402
+ - Per-invitation semantics identical to `createAppMagicLink` (shared `createOneAppMagicLink` helper): `redirectPath` is omitted from the nimbus-ai call when absent, `addToAccessPrincipals` defaults to true upstream.
403
+ - Permission: `app_magic_link.write` with org-scoped authz (same as `createAppMagicLink`). No nimbus-ai changes — the gate fans out the existing per-user calls.
404
+ - MCP: `bulkCreateAppMagicLinks` is MCP-visible under the `appMagicLinks` prompt group; the prompt (v1.4.0) now tells agents to use it instead of looping `createAppMagicLink`.
405
+
406
+ ### Workspace and portal member read
407
+
408
+ - Added `listWorkspaceMembers({ orgId, workspaceId })`.
409
+ - Added `listPortalMembers({ orgId, workspaceId })` (portal membership is represented by its underlying workspace, same semantics as `removePortalMember`).
410
+ - Both return `{ members: OrgWorkspaceMember[] }`, where each member includes the workspaceMember `id` (globalId) required by `removeWorkspaceMember` / `removePortalMember`.
411
+
412
+ ### Add Portal Blank Note Block
413
+
414
+ - `POST /:orgId/portals/:portalId/pages/:pageId/blocks/blank-note` (`addPortalBlankNoteBlock`,
415
+ permission `portals.write`, MCP prompt groups `authz`, `sdk`, `portals`).
416
+ - Optional body `{ title }` (default `"New Document"`); returns
417
+ `{ noteId, blockId, pageId, branchId, seqs, staged: true }`.
418
+ - The whole use case lives in portal-service (`POST /portals/:portalId/pages/:pageId/blocks/blank-note`,
419
+ portal-service client `2.34.0`): note creation/sharing, canonical block shape, block
420
+ index from the effective draft state and the draft append. Gate makes exactly one
421
+ downstream call and adds no customizer business logic.
422
+ - Unlike `addPortalNoteBlock`, `pageId` may also be a page created earlier in the
423
+ active draft (portal-service resolves published + staged pages).
424
+
425
+ ### Conditional removeOrgMember (optional preconditions)
426
+
427
+ - `OrgUsersApi.removeOrgMember` gained an optional `query` argument (`expectedRole`, `expectedJoinedAfter`).
428
+ - New exported types `OrgMemberExpectedRoleInQueryOptional`, `OrgMemberExpectedJoinedAfterInQueryOptional`.
429
+ - OpenAPI: two optional query parameters on `removeOrgMember`.
430
+
431
+ ### Add portal Chat block + channel discovery
432
+
433
+ - Added `listPortalChatChannels` and `addPortalChatBlock` operations + SDK methods.
434
+ - Permission: `portals.write`, org-scoped authz.
435
+ - **BREAKING:** generic `addPortalBlock` no longer accepts `type='chat'`. Consumers
436
+ must migrate to `listPortalChatChannels` (for a channel target) followed by
437
+ `addPortalChatBlock`.
438
+
439
+ ### Add Portal Embed Block
440
+
441
+ - `POST /:orgId/portals/:portalId/pages/:pageId/blocks/embed` (`addPortalEmbedBlock`,
442
+ permission `portals.write`, MCP prompt groups `authz`, `sdk`, `portals`).
443
+ - Body `{ title, embedType, url?, content?, showEmbedLink?, colspan?, rowspan?, height? }`:
444
+ - `embedType` — one of the 11 chooser variants (`custom`, `youtube`, `google-drive`,
445
+ `ms-doc`, `calendly`, `hubspot`, `airtable`, `loom`, `data-studio`, `figma`, `miro`).
446
+ The legacy `unknown` is rejected.
447
+ - For `custom`, pass `content` (URL or embed code) and not `url`; for every named
448
+ variant, pass `url` and not `content`. The service builds the canonical iframe.
449
+ - Defaults: `showEmbedLink` true, `colspan` 3, `rowspan` 1, `height` null.
450
+ - Returns `{ blockId, pageId, branchId, seqs, staged: true }`.
451
+ - The whole use case lives in portal-service (`POST /portals/:portalId/pages/:pageId/blocks/embed`):
452
+ semantic-input validation, URL/Custom-Embed normalization, canonical iframe/block
453
+ shape, index and the draft append. Gate makes exactly one downstream call and adds
454
+ no customizer business logic. No server-side fetch, DNS resolution or Iframely
455
+ request is performed.
456
+ - Like `addPortalBlankNoteBlock`, `pageId` may also be a page created earlier in the
457
+ active draft (portal-service resolves published + staged pages).
458
+
459
+ ### createPortalPageWithNote moves into portal-service
460
+
461
+ - No public contract change: `POST /:orgId/portals/:portalId/pages/note`
462
+ (`createPortalPageWithNote`) keeps its operationId, path, request/response fields
463
+ and MCP prompt (`authz`, `sdk`, `portals`). Response is unchanged
464
+ (`{ noteId, menuItemId, pageId, url, branchId, seqs, staged: true }`).
465
+ - The contract now declares `403` (portal console / org access denied) and `409`
466
+ (draft conflict after the internal retry) as documented responses, and enforces
467
+ `title` `maxLength 100`.
468
+ - Gate makes exactly one downstream call and no longer imports customizer draft/event
469
+ types; note-service, editor-server, menu/slug/index and draft sequencing are gone
470
+ from the controller.
471
+ - portal-service response fields (`noteId`, `menuItemId`, `pageId`, `url`, `branchId`,
472
+ `seqs`; `lastSeq` on 409) are marked required in its OpenAPI, so the generated client
473
+ exposes them as non-optional.
474
+
475
+ ### List portal Chat DM target users
476
+
477
+ - Added `listPortalChatUsers` operation + SDK method.
478
+ - Permission: `portals.write`, org-scoped authz. The org member search is
479
+ user-bound (org-service enforces `canViewMembers` on the acting user).
480
+ - Query params: `query` (optional, 2..100 chars, matched case-insensitively
481
+ against display name, username and email), `limit` (optional, default 20,
482
+ max 50), `cursor` (optional, opaque, bound to org+user+query).
483
+ - MCP `portals` prompt v1.23.0: added `listPortalChatUsers` guidance, the
484
+ DM-target flow for `addPortalChatBlock`, and the one-page-at-a-time browse /
485
+ "show the next 20?" pagination rule.
486
+
487
+
488
+ ## Consumer Impact
489
+
490
+ ### 2026-05-20-app-magic-link-redirectpath-fix
491
+
492
+ - `createAppMagicLink` and `requestAppMagicLink` calls that omit
493
+ `redirectPath` now succeed (HTTP 200) instead of failing with HTTP 400
494
+ `"must be string"`. Callers that already send a `redirectPath` are
495
+ unaffected.
496
+ - No action required by SDK or apps-cli consumers — the wire contract is
497
+ unchanged; this purely fixes the no-deep-link request path.
498
+
499
+ ### Isolated store secret guidance
500
+
501
+ App agents should resolve isolated stores at runtime through Gate using app token/source scope and stable aliases, or use platform-provided bindings when available.
502
+
503
+ Operator/CI migration commands may still use `storeId` in handoff logs, `fusebase.json`, or `--store-id`; that does not make `storeId` app runtime configuration.
504
+
505
+ ### Get organization URL
506
+
507
+ - Apps and agents can stop hardcoding Fusebase hostnames when building org-scoped links.
508
+ - Portal domains and app magic-link hosts remain separate surfaces; use `listPortals` or app APIs for those.
509
+
510
+ ### createPortalFolder
511
+
512
+ - Agents can create a portal folder without touching the customizer UI. The op
513
+ stages two change events exactly as the customizer's section-create flow does:
514
+ an `addPage` node followed by an `addMenuItem` (`portalSection`) pointing at
515
+ it, sharing a `batchId` so the folder is a single undo unit. The slug is
516
+ derived from the name (collision-suffixed against the existing menu) and the
517
+ item is appended to the end of the root sidebar.
518
+ - The change is staged, not live — consumers must tell the user to publish it.
519
+
520
+ ### duplicatePortalItem
521
+
522
+ - Agents can duplicate portal content without the customizer UI. The op reads the
523
+ published menu subtree (and each page's content blocks) and re-emits fresh
524
+ `addPage` / `addMenuItem` / `addBlock` change events with new ids, remapping
525
+ parent ids so the copied subtree is self-contained. Copyable types are
526
+ `portalSection`, `note`, `portalPage`, `notesFolder`, `link`; portal
527
+ scaffolding (home, system dashboards, tasks/chat/all-pages, processes) is
528
+ skipped.
529
+ - Fusebase notes are **reused** (the copies reference the same note via
530
+ `targetId`, the note is not cloned), so later edits to the note affect both the
531
+ original and the copy.
532
+ - It reads **published** content only; pending draft changes should be published
533
+ before duplicating items that depend on them.
534
+ - The change is staged, not live — consumers must tell the user to publish it.
535
+
536
+ ### updatePortalAccess
537
+
538
+ - Agents can set portal access without touching the customizer UI. The op maps
539
+ the high-level mode to portal-service `access.portal` flags exactly as the
540
+ customizer's `PortalAccessSelector` does; `access.console` (editor access) is
541
+ preserved.
542
+ - The change is staged, not live — consumers must tell the user to publish it.
543
+
544
+ ### Bulk invite users to a portal
545
+
546
+ - App agents inviting several clients can issue one call instead of N blocking sequential requests.
547
+ - Single `inviteToPortal` is unchanged; behavior and response shape are preserved.
548
+ - Failures are isolated per invitee in `completed` mode — a single bad invite does not fail the batch.
549
+
550
+ ### getMe returns user timezone and time-format preferences
551
+
552
+ - Apps can render dates/times in the user's timezone and clock format directly from `getMe`.
553
+ - Backward compatible: existing fields are unchanged; `preferences` is additive.
554
+ - Data sources: timezone from user-service `GET /users/{id}/timezone`; `timeFormat` derived from the
555
+ `dateTimeLocale` user var (defaults to `en-US` → `12h` when unset), matching the webnotes client.
556
+ - Preference resolution is non-fatal: if user-service is unreachable, `getMe` still returns identity with `preferences: null`.
557
+
558
+ ### Token creation accepts isolated_store RLS break-glass permissions
559
+
560
+ - Studio and other token-creation UIs can include RLS break-glass permissions when issuing owner/manager tokens.
561
+ - No migration or config changes required.
562
+
563
+ ### List an app's portal embeds
564
+
565
+ - public-api can proxy this op for the CLI; the frontend Resources popup and in-app runtime can fetch an app's portal embeds via the regenerated SDK.
566
+ - Org-scoped by design (per spec): returns all embeds in the org; workspace-level access enforcement remains portal-service's responsibility (same as `listPortals`).
567
+ - App-token callers must have `portals.read` in their scoped permissions, otherwise the token path returns 403.
568
+ - Requires `npm run build:sdk` to publish the new op in the SDK/OpenAPI.
569
+
570
+ ### publishPortalDraft
571
+
572
+ - Agents can publish staged portal changes without the customizer UI. The op
573
+ delegates to portal-service `POST /portals/:id/draft/publish`, which applies the
574
+ whole active draft branch through the same content pipeline the customizer's
575
+ Publish button uses (every reducer/cache/share side effect preserved).
576
+ - It publishes the ENTIRE staged draft (including any changes the owner staged
577
+ manually in the customizer), so it should only be called when the user
578
+ explicitly wants the staged changes to go live.
579
+ - Returns 400 if the draft is empty (nothing staged) and 409 if the portal was
580
+ published elsewhere since the draft was based (the draft is stale and must be
581
+ re-staged before publishing again).
582
+
583
+ ### App portal embeds — detect iframe embeds, add `kind`
584
+
585
+ - public-api / CLI / frontend receive the new `kind` field; downstream consumers
586
+ pass it through without behavioral or visual differentiation for now.
587
+ - Requires `npm run build:sdk` to publish the additive `kind` field and the updated
588
+ op description.
589
+
590
+ ### Append workspace note content
591
+
592
+ Apps can now update existing notes by appending text or HTML instead of creating a new note. For Markdown sources, apps should render Markdown to HTML before calling the operation when formatting fidelity matters; otherwise send the content as text.
593
+
594
+ ### Client → client app invite op (NIM-40541 / NIM-41954)
595
+
596
+ - The gate forwards the inviter's id to nimbus-ai, which performs the actual
597
+ "inviter has app access" check (strict principal match — never the permissive
598
+ managed-app User fallback) and creates the invite.
599
+
600
+ ### Client → client portal invite authz (NIM-40541 / NIM-41950)
601
+
602
+ - Portal-client UI (NIM-41952) maps the above error codes to user messages.
603
+ - A client invitee is always forced to `orgRole='client'` and inherits the
604
+ inviter's binary access level (full-private vs shared-only); the request body's
605
+ `orgRole`/`isFullAccess` are ignored for client callers.
606
+
607
+ ### Markdown (v3) notes CRUD
608
+
609
+ AI/MCP callers can manage markdown-native (v3) notes end-to-end without HTML conversion. Classic (v2) notes are untouched and keep using the existing notes operations. The operations depend on the note-service markdown API (NIM-42040/NIM-42041); until note-service ships it, calls fail upstream.
610
+
611
+ ### Member removal and client deletion scheduling
612
+
613
+ Consumers can remove org members by numeric `userId`, remove workspace/portal members by `workspaceId` plus numeric `userId`, and schedule Client account deletion by numeric user id in the org context. Gate resolves internal membership ids before calling org-service.
614
+
615
+ ### Bulk app magic-link invites
616
+
617
+ - App agents inviting several clients issue one call instead of N blocking sequential requests.
618
+ - Single `createAppMagicLink` is unchanged; behavior and response shape are preserved.
619
+ - Failures are isolated per invitee in `completed` mode — one bad invite does not fail the batch.
620
+
621
+ ### Workspace and portal member read
622
+
623
+ Apps can list members of a specific facility workspace/portal instead of the whole-org roster, avoiding cross-facility leakage, and can obtain the membership `id` needed to remove a workspace/portal member. Both ops require `org.members.read` and org access. A workspace outside the org returns `404`.
624
+
625
+ ### Add Portal Blank Note Block
626
+
627
+ - Additive: no existing operation changes. `addPortalNoteBlock` stays the way to
628
+ embed an already-created, already-shared note.
629
+ - Requires portal-service `>= 2.34.0` deployed; on an older portal-service the
630
+ operation returns 404 from the downstream route.
631
+
632
+ ### Conditional removeOrgMember (optional preconditions)
633
+
634
+ - Existing callers are unaffected — omitting both params keeps today's behavior.
635
+ - Invite-cancel flows should capture a timestamp before inviting and pass it as `expectedJoinedAfter` (plus the invited `expectedRole`), so undoing an invite can never drop a pre-existing or since-promoted member.
636
+ - `expectedJoinedAfter` is in **seconds**, matching org-service `member.createdAt`. Passing milliseconds fails closed (409, nothing removed).
637
+
638
+ ### Add portal Chat block + channel discovery
639
+
640
+ - Agents can add a Chat block by exact channel id or a DM user id without touching
641
+ chat-backend directly.
642
+ - Any caller passing `type='chat'` to `addPortalBlock` now gets a 400 and must use
643
+ `addPortalChatBlock`.
644
+
645
+ ### Add Portal Embed Block
646
+
647
+ - Requires portal-service with the `/blocks/embed` route deployed; on an older
648
+ portal-service the operation returns 404 from the downstream route.
649
+ - Other `addPortalBlock` types are unchanged.
650
+
651
+ ### createPortalPageWithNote moves into portal-service
652
+
653
+ - Backward compatible: existing callers/agents see the same operation and response.
654
+ - Requires the portal-service release that adds `POST /portals/:portalId/pages/note`
655
+ deployed first; on an older portal-service the operation returns 404 from the
656
+ downstream route. Deploy portal-service before this Gate change.
657
+
658
+ ### List portal Chat DM target users
659
+
660
+ - Agents can resolve a DM target by name, username or email — or browse the
661
+ members when the person is not known — without guessing a user id and without
662
+ loading the full organization user list.
663
+ - `email` is returned so an agent can distinguish two users sharing a display name.
664
+
665
+
666
+ ## Verification
667
+
668
+ ### 2026-05-20-app-magic-link-redirectpath-fix
669
+
670
+ - `npm run build` — clean.
671
+ - `npm run lint` — 0 errors (5 pre-existing `dist/` warnings).
672
+ - `npm test` — 216 pass / 1 skipped. `tests/unit/app-magic-links-controller.test.ts`
673
+ updated: the two tests that previously asserted `redirectPath: null` on the
674
+ wire now assert the key is **absent** when omitted, plus a new case that an
675
+ empty-string `redirectPath` is forwarded verbatim (19 cases in the suite).
676
+
677
+ ### 2026-05-27 — Visitor access vs open `/api/*` (skill guidance)
678
+
679
+ - `npm test -- --runInBand tests/unit/mcp-prompts.test.ts`
680
+ - `npm run mcp:skills:generate`
681
+ - `npm run mcp:skills:validate`
682
+ - `npm run mcp:skills:copy-to-apps-cli:local` (optional)
683
+
684
+ ### Isolated store secret guidance
685
+
686
+ - Ran `npm run mcp:skills:generate`.
687
+ - `npm run mcp:skills:validate` is blocked in this environment because the `skills-ref` Python module is missing.
688
+
689
+ ### Get organization URL
690
+
691
+ - `npm test -- tests/unit/org-url-service.test.ts tests/unit/orgs-controller.test.ts`
692
+ - `npm run build:sdk`
693
+ - `npm run mcp:skills:generate`
694
+ - `npm run mcp:skills:validate`
695
+
696
+ ### createPortalFolder
697
+
698
+ - `npm run build` (regenerates runtime schema defs, tsc).
699
+ - `npm test` — unit tests pass, incl. new `createPortalFolder` coverage
700
+ (batch order, slug collision, icon, 409 retry, empty-name 400, 404).
701
+ - `npm run lint` — clean.
702
+
703
+ ### duplicatePortalItem
704
+
705
+ - `npm run build:sdk` / `npm run build` (regenerates schema defs + SDK, tsc).
706
+ - `npm test` — unit tests pass, incl. new `duplicatePortalItem` coverage
707
+ (subtree copy with fresh ids + reused note + detached block, name override,
708
+ item-not-found 400, non-copyable-type 400, graceful block-read failure, 404).
709
+ - `npm run lint` — clean (only pre-existing `no-console` warnings).
710
+
711
+ ### updatePortalAccess
712
+
713
+ - `npm run build` (regenerates runtime schema defs, tsc).
714
+ - `npm test` — 265 unit tests pass, incl. new `updatePortalAccess` coverage
715
+ (mode→flags mapping, console preservation, 409 retry, 404).
716
+ - `npm run lint` — clean.
717
+
718
+ ### Bulk invite users to a portal
719
+
720
+ - `npm test -- tests/unit/portals-bulk-invite-controller.test.ts tests/unit/concurrency.test.ts tests/unit/portals-controller.test.ts`
721
+ - `npm run build`
722
+ - `npm run build:sdk`
723
+ - `npm run mcp:skills:generate` (validate blocked locally: `skills-ref` Python module unavailable in this environment)
724
+
725
+ ### getMe returns user timezone and time-format preferences
726
+
727
+ - `npm run build`
728
+ - `npm test -- tests/unit/me-controller.test.ts`
729
+
730
+ ### Token creation accepts isolated_store RLS break-glass permissions
731
+
732
+ - `npm test -- tests/unit/permissions.test.ts`
733
+ - Create a token via `POST /tokens` with `isolated_store.rls.bypass` in `permissions`.
734
+
735
+ ### List an app's portal embeds
736
+
737
+ - `eslint` and `tsc --noEmit` clean on the changed files.
738
+ - Runtime (user): `GET /:orgId/apps/:appId/portal-embeds` with an internal user and with an app token — returns the deduped per-page list; a user lacking `portals.read` is denied.
739
+
740
+ ### publishPortalDraft
741
+
742
+ - `npm run build` (regenerates runtime schema defs, tsc).
743
+ - `npm run build:sdk` (regenerates SDK + OpenAPI, with isolated-store feature flags).
744
+ - `npm test` — unit tests pass, incl. new `publishPortalDraft` coverage
745
+ (success, empty-draft 400, stale 409, portal-not-found 404).
746
+ - `npm run lint` — clean.
747
+
748
+ ### App portal embeds — detect iframe embeds, add `kind`
749
+
750
+ - `eslint` and `tsc --noEmit` clean on the changed files (whole-project tsc: 0
751
+ errors).
752
+ - Runtime (user): embed an app via a regular `embed` block (managed or custom host,
753
+ with path/query) → page appears with `kind: 'iframe'`; brick + iframe on the same
754
+ page → one `app-feature` row; nimbus-ai down / app unpublished → brick results
755
+ returned normally. Works for internal-user and app-token auth.
756
+
757
+ ### Append workspace note content
758
+
759
+ - `npm run build:sdk`
760
+ - `npm run mcp:skills:generate`
761
+ - `npm run mcp:skills:validate`
762
+ - `npm run mcp:skills:copy-to-apps-cli:local`
763
+
764
+ ### Client → client app invite op (NIM-40541 / NIM-41954)
765
+
766
+ - `npm run build`, `npm run build:sdk` (generated SDK tsc green).
767
+ - Unit test: gate controller forwards the inviter id and maps the no-context
768
+ case to 403.
769
+
770
+ ### Client → client portal invite authz (NIM-40541 / NIM-41950)
771
+
772
+ - `npm test` (controller authz matrix + inheritance, 16 cases in
773
+ `tests/unit/portals-bulk-invite-controller.test.ts`).
774
+ - `npm run build`, `npm run lint` (no new warnings).
775
+
776
+ ### Markdown (v3) notes CRUD
777
+
778
+ - `npm run build`
779
+ - `npm run build:sdk`
780
+ - `npm run mcp:skills:generate`
781
+ - `npm run mcp:skills:validate`
782
+ - `npm test` (unit suites for the new controller, client, and contracts)
783
+
784
+ ### Member removal and client deletion scheduling
785
+
786
+ - `npm test -- --runTestsByPath tests/unit/org-users-controller.test.ts`
787
+ - `npm run build:sdk`
788
+ - `npm run build`
789
+ - `npm run mcp:skills:generate`
790
+ - `npm run mcp:skills:validate`
791
+
792
+ ### Bulk app magic-link invites
793
+
794
+ - `npm run build`
795
+ - `npm run build:sdk`
796
+ - `npm run mcp:skills:generate` + `npm run mcp:skills:validate`
797
+ - `npm test`, `npm run lint`
798
+ - Unit tests for the bulk controller follow in NIM-42093.
799
+
800
+ ### Workspace and portal member read
801
+
802
+ - `npm test -- --runTestsByPath tests/unit/org-users-controller.test.ts`
803
+ - `npm run build:sdk`
804
+ - `npm run build`
805
+ - `npm run mcp:skills:generate`
806
+ - `npm run mcp:skills:validate`
807
+
808
+ ### getMe no longer requires health.read
809
+
810
+ - `npm run test:unit -- tests/unit/authz-op-registry-bridge.test.ts`
811
+ - `npm run build`
812
+
813
+ ### Add Portal Blank Note Block
814
+
815
+ - `npx tsc --noEmit`, `npm run lint`, `npx jest tests/unit/portals-controller.test.ts` (84 passed).
816
+ - `npm run mcp:skills:generate` + `npm run mcp:skills:validate`.
817
+ - portal-service: `npx jest lib/controllers/contents/addBlankNoteBlock.spec.ts` (13 passed).
818
+
819
+ ### Conditional removeOrgMember (optional preconditions)
820
+
821
+ - `npm test`
822
+ - `npm run build`
823
+ - `npm run mcp:skills:generate && npm run mcp:skills:validate`
824
+
825
+ ### Add portal Chat block + channel discovery
826
+
827
+ - `npm test -- tests/unit/portals-contracts.test.ts tests/unit/portals-controller.test.ts`
828
+ - `npm run build`
829
+
830
+ ### Add Portal Embed Block
831
+
832
+ - portal-service: `npx jest lib/controllers/contents/embedNormalization.spec.ts
833
+ lib/controllers/contents/addPortalEmbedBlock.spec.ts` (green); `npm run build` green.
834
+ - gate: `npx tsc --noEmit`, `npm run lint`, `npx jest tests/unit/portals-controller.test.ts
835
+ tests/unit/portals-contracts.test.ts tests/unit/portals-embed-block-seam.test.ts`.
836
+ - `npm run mcp:skills:generate` + `npm run mcp:skills:validate`.
837
+
838
+ ### createPortalPageWithNote moves into portal-service
839
+
840
+ - portal-service: `npx jest lib/controllers/contents/createPortalPageWithNote.spec.ts`.
841
+ - gate: `npm run build`, `npm run lint`,
842
+ `npm test -- tests/unit/portals-controller.test.ts tests/unit/portals-contracts.test.ts tests/unit/portals-page-with-note-seam.test.ts`.
843
+ - `npm run mcp:skills:generate` + `npm run mcp:skills:validate`.
844
+
845
+ ### List portal Chat DM target users
846
+
847
+ - `npm test -- tests/unit/portals-contracts.test.ts tests/unit/portals-chat-block-seam.test.ts`
848
+ - `npm run build`
849
+
850
+
851
+ ## Follow-ups
852
+
853
+ ### 2026-05-20-app-magic-link-redirectpath-fix
854
+
855
+ - None. End-to-end QA re-runs the no-`redirectPath` invite and self-service
856
+ cases against the dev Gate after redeploy.
857
+
858
+ ### Isolated store secret guidance
859
+
860
+ - Restore/install the local `skills_ref` validator dependency so MCP skill validation can run end to end.
861
+
862
+ ### Get organization URL
863
+
864
+ - None.
865
+
866
+ ### createPortalFolder
867
+
868
+ - No new portal-service endpoint was needed: reuses the existing draft endpoints
869
+ - `addPage`/`addMenuItem` reducer. Sibling content ops in NIM-40621
870
+ (createPortalPageWithNote, addPortalNoteBlock, duplicatePortalItem) follow the
871
+ same draft-staging pattern.
872
+
873
+ ### duplicatePortalItem
874
+
875
+ - No new portal-service endpoint was needed: reuses the existing draft endpoints
876
+ - `addPage`/`addMenuItem`/`addBlock` reducer and the `GET /blocks` reader.
877
+ - A future iteration could deep-clone notes and replicate sync/permission/process
878
+ fidelity (closer to `apiPostMenusCopy`).
879
+
880
+ ### updatePortalAccess
881
+
882
+ - No new portal-service endpoint was needed: reuses the existing draft endpoints
883
+ - `updateAccessSettings` reducer. Sibling content ops in NIM-40621 follow the
884
+ same draft-staging pattern.
885
+
886
+ ### Bulk invite users to a portal
887
+
888
+ - Background mode is fire-and-forget within the request pod; if durable retry/observability is later required, move bulk processing to a worker/queue.
889
+
890
+ ### getMe returns user timezone and time-format preferences
891
+
892
+ - SDK artifact regeneration (`npm run build:sdk`) is currently blocked by a pre-existing
893
+ IsolatedStores SDK drift (`generated/sdk-client/src/extras/sqlMigrationBundle.ts` imports
894
+ `IsolatedStoreSqlMigrationBundleContract`, which the regenerator no longer emits). The published
895
+ SDK should be rebuilt by the SDK publish pipeline once that drift is resolved.
896
+
897
+ ### Token creation accepts isolated_store RLS break-glass permissions
898
+
899
+ - None.
900
+
901
+ ### List an app's portal embeds
902
+
903
+ - Regenerate the SDK (`npm run build:sdk`) before downstream (public-api / frontend) consume the op.
904
+ - M3 (public-api proxy) and M4 (frontend Resources row) are separate milestones.
905
+
906
+ ### publishPortalDraft
907
+
908
+ - No new portal-service endpoint was needed: reuses the existing
909
+ `POST /portals/:id/draft/publish` (NIM-41548). Sibling content ops in NIM-40621
910
+ stage into the same draft this op publishes.
911
+
912
+ ### App portal embeds — detect iframe embeds, add `kind`
913
+
914
+ - Regenerate + publish the SDK (`npm run build:sdk` / `build:sdk:push`); note the
915
+ version for the M3A (public-api) and M4A (nx-frontend) pin bumps.
916
+ - M3A/M4A/M5A pass `kind` through unchanged.
917
+
918
+ ### Append workspace note content
919
+
920
+ - Full note replacement/editing remains a separate product/API decision because it needs a canonical Markdown-to-editor-content conversion path.
921
+
922
+ ### Client → client portal invite authz (NIM-40541 / NIM-41950)
923
+
924
+ - Enable `FEATURE_FLAGS=client_invite` per environment (dev for QA, prod on
925
+ rollout) — the feature is fail-closed until then.
926
+ - App client→client invite op is tracked separately (NIM-41954, ST-5).
927
+
928
+ ### Markdown (v3) notes CRUD
929
+
930
+ - Switch the plain-HTTP note-service markdown client to the generated `@internal/note-service` SDK once note-service publishes the markdown endpoints.
931
+ - Add e2e-sdk/mcp-e2e coverage once the note-service contract is live (fake-note-service markdown routes should mirror the real service).
932
+
933
+ ### Member removal and client deletion scheduling
934
+
935
+ - None.
936
+
937
+ ### Bulk app magic-link invites
938
+
939
+ - Background mode is fire-and-forget within the request pod; if durable retry/observability is later required, move bulk processing to a worker/queue.
940
+
941
+ ### Workspace and portal member read
942
+
943
+ - App-side Ovation Benchmarking integration is a separate task once Gate ships.
944
+
945
+ ### Add Portal Blank Note Block
946
+
947
+ - No automatic unshare/remove compensation: a failed share or a terminal draft
948
+ append can leave an empty orphan note (logged as a structured warning with
949
+ phase/portalId/pageId/workspaceId/noteId/blockId). Cleanup is tracked separately.
950
+ - SDK/OpenAPI artifacts are regenerated by the release `build(sdk)` commit, not here.
951
+
952
+ ### Conditional removeOrgMember (optional preconditions)
953
+
954
+ - Query-parameter schemas are referenced but not emitted into `components/schemas` by the OpenAPI generator (pre-existing — same for `EmailInQueryOptional`).
955
+
956
+ ### Add portal Chat block + channel discovery
957
+
958
+ - `listPortalChatUsers` (DM user search) + the dedicated org-service search endpoint
959
+ are tracked separately: org-service member rows carry no name fields, so a proper
960
+ name search needs a new org-service endpoint joining user profiles + an SDK
961
+ regenerate/publish. The DM target already works end-to-end via `addPortalChatBlock`
962
+ when the caller has the userId.
963
+
964
+ ### Add Portal Embed Block
965
+
966
+ - Private/loopback rejection is a literal-IP/well-known-name check (no DNS
967
+ resolution, per spec); a hostname that resolves to a private address is not caught.
968
+ - SDK/OpenAPI artifacts are regenerated by the release `build(sdk)` commit, not here.
969
+
970
+ ### createPortalPageWithNote moves into portal-service
971
+
972
+ - No automatic unshare/remove compensation: a failed share, initial-content paste or
973
+ terminal draft append can leave an orphan note (logged as a structured warning with
974
+ phase/portalId/parentId/workspaceId/noteId/menuItemId/pageId/blockId). Cleanup is
975
+ tracked separately.
976
+ - SDK/OpenAPI artifacts are regenerated by the release `build(sdk)` commit, not here.
977
+
978
+
979
+ ## Breaking
980
+
981
+ ### Add Portal Embed Block
982
+
983
+ - `addPortalBlock` **rejects `type: "embed"`**. Direct callers that previously staged
984
+ an embed via the generic op must migrate:
985
+
986
+ ```
987
+ // before
988
+ addPortalBlock({ pageId, type: "embed", title,
989
+ properties: { embedType, content, contentUrl, showEmbedLink } });
990
+
991
+ // after — named variant
992
+ addPortalEmbedBlock({ orgId, portalId, pageId, title, embedType, url });
993
+ // after — custom embed
994
+ addPortalEmbedBlock({ orgId, portalId, pageId, title, embedType: "custom", content });
995
+ ```
996
+
997
+ `contentUrl` and the iframe are no longer supplied by the caller — the service
998
+ generates them from `url` (named) or stores `content` verbatim (custom).
999
+
1000
+
1001
+ ## Changes
1002
+
1003
+ ### 2026-05-27 — Visitor access vs open `/api/*` (skill guidance)
1004
+
1005
+ - `src/mcp/prompts/fusebase-auth.ts` — `1.1.0` → `1.2.0`: section **Visitor Access Vs Open API (Platform Edge)**.
1006
+ - `src/mcp/prompts/app-magic-links.ts` — `1.2.0` → `1.3.0`: section **Platform Edge: Visitor Token And `/api/*`**; tighten `visitor` principal wording.
1007
+ - Regenerated `generated/claude_skills/fusebase-gate/references/{fusebase-auth,app-magic-links}.md`.
1008
+
1009
+
1010
+ ## Consumer impact
1011
+
1012
+ ### 2026-05-27 — Visitor access vs open `/api/*` (skill guidance)
1013
+
1014
+ Documentation / agent guidance only — no API or permission changes.
1015
+
1016
+ ### getMe no longer requires health.read
1017
+
1018
+ - **Apps without `health.read` in the published grant** can call `getMe` again (service tokens, visitor browser tokens).
1019
+ - **Apps that already include `health.read`** are unchanged.
1020
+ - Do **not** remove `health.read` from grants solely because of this change — other ops may still require it.
1021
+ - Continue using `FBS_ORG_ID` / `getMyOrgAccess` for org resolution; do not rely on `getMe().auth.scopes` for visitor sessions.
1022
+
1023
+
1024
+ ## Dependency
1025
+
1026
+ ### Client → client app invite op (NIM-40541 / NIM-41954)
1027
+
1028
+ - Requires the nimbus-ai endpoint `apiCreateAppClientInviteMagicLink`
1029
+ (`POST /orgs/:org/apps/:appId/client-invites`, secret/superuser-trusted),
1030
+ shipped on the nimbus-ai `NIM-40541` branch. Until it deploys, this op returns
1031
+ the upstream error.
1032
+
1033
+
1034
+ ## Limitations (iteration 1)
1035
+
1036
+ ### duplicatePortalItem
1037
+
1038
+ - Same-portal only; cross-portal copy is out of scope.
1039
+ - Notes are referenced, not deep-cloned.
1040
+ - Sync-origin linkage, external-resource copies, process status, and per-page
1041
+ permission copies (handled by portal-service's heavyweight `apiPostMenusCopy`)
1042
+ are not replicated — copies use allow-all permissions and are detached from
1043
+ any sync origin. The owner reviews before publishing.
1044
+ - A copy that would exceed the portal-service draft cap (200 change events) is
1045
+ rejected with a clear 400.
1046
+
1047
+
1048
+ ## Why
1049
+
1050
+ ### getMe no longer requires health.read
1051
+
1052
+ - `getMe` is the primary identity introspection op for Fusebase Apps.
1053
+ - Requiring `health.read` caused production lockouts when app grants omitted it after `fusebase analyze gate` / `--sync-gate-permissions` (see issue `041-benchmarking-app`, escalation `2026-07-13-platform-gate-2.3.29-auth-breaking-changes.md`).
1054
+ - `resolveOperationPermissions` now reports `required_permission: null` for `getMe`, aligned with `getHealth`.