@collective-intelligence/external-apps-server-sdk 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -27,8 +27,10 @@ subpaths:
27
27
  The SDK has three separate authentication boundaries:
28
28
 
29
29
  - `createServerClient` uses confidential service credentials for requests from
30
- the customer server to CI. Keep those credentials on the server and use the
31
- only supported client mode, `service`. Its `tenantUrl` must be a
30
+ the customer server to CI. Keep those credentials on the server and use
31
+ client mode `service`, or delegated mode with a member access token from
32
+ [`exchangeMemberSession`](#call-ci-as-a-member-your-server-signed-in) to act
33
+ as a member. Its `tenantUrl` must be a
32
34
  credential-free HTTPS origin; for local development it also accepts an
33
35
  `http` origin whose host is exactly `localhost`, a `127.0.0.0/8` address, or
34
36
  `[::1]`. Any other `http` host is rejected before credentials are sent.
@@ -45,8 +47,8 @@ through `/admin`. None of these credentials or grants belongs in a Gateway
45
47
  manifest.
46
48
 
47
49
  A confidential application created by package install has no `clientSecret`
48
- until an administrator with `public_applications:manage` rotates its credential
49
- (`POST /admin/organization/public-applications/{id}/credential/rotate`, or
50
+ until an administrator with `external_apps:manage` rotates its credential
51
+ (`POST /admin/organization/external-apps/{id}/credential/rotate`, or
50
52
  **Rotate secret** in the Dashboard). CI returns the secret once; store it
51
53
  server-side. See
52
54
  [Provision a secret for a package-created application](../../../docs/reference/package-release-cli.md#provision-a-secret-for-a-package-created-application).
@@ -135,6 +137,61 @@ Override `validate`, `idempotencyKey`, `authorize`, or `getAccessToken` when
135
137
  the application needs different policy. The deprecated `./express` entry
136
138
  re-exports only `createGatewayExpressMiddleware` from `./runtime/express`.
137
139
 
140
+ ## Call CI as a member your server signed in
141
+
142
+ A server that signs members in to CI itself (for example through
143
+ `POST /auth/otp/verify`) holds each member's CI session token. CI application
144
+ APIs do not accept that session token. Exchange it for a short-lived member
145
+ access token with `exchangeMemberSession`, then call CI through a delegated
146
+ `createServerClient`:
147
+
148
+ ```ts
149
+ import {
150
+ createServerClient,
151
+ exchangeMemberSession,
152
+ MemberSessionExchangeError
153
+ } from "@collective-intelligence/external-apps-server-sdk"
154
+
155
+ const { accessToken, expiresAt } = await exchangeMemberSession({
156
+ credentials: { clientId, clientSecret },
157
+ organizationId,
158
+ scopes: ["context:read", "knowledge:write"],
159
+ sessionToken: member.ciSessionToken,
160
+ tenantUrl: "https://ci.example.com"
161
+ })
162
+ const ci = createServerClient({
163
+ authentication: { getAccessToken: () => accessToken },
164
+ tenantUrl: "https://ci.example.com"
165
+ })
166
+ await ci.context.get({ organizationId })
167
+ ```
168
+
169
+ Only a confidential application can exchange, with its client secret. CI
170
+ applies the same checks as the browser login: the session is live, the member
171
+ is not banned and belongs to the Organization, the application is enabled and
172
+ allowed in that Organization, and the requested scopes are a non-empty subset
173
+ of the application's scopes. The token acts as the member (`detached_user`),
174
+ lasts 300 seconds, has no refresh token, and stops working when the member's
175
+ CI session ends. `expiresAt` is counted from before the request and set 30
176
+ seconds early, so exchange again once it passes; never send either token to
177
+ the browser.
178
+
179
+ The request and its response body are bounded by `timeoutMs` (10 seconds by
180
+ default); a CI that stops answering ends in `temporarily_unavailable`. Pass
181
+ `signal` to cancel: a cancelled exchange rejects with the signal's reason, never
182
+ with a retryable `MemberSessionExchangeError`.
183
+
184
+ A refusal throws `MemberSessionExchangeError`. `code` is CI's OAuth error:
185
+ `invalid_grant` means the CI session ended or the member cannot act in the
186
+ Organization, so sign the member in again. `invalid_client`,
187
+ `unauthorized_client`, `invalid_scope` and `invalid_target` point at the
188
+ application's credentials, type, scopes or allowed Organizations.
189
+ `temporarily_unavailable` is retryable. On the wire this is the RFC 8693 grant
190
+ `urn:ietf:params:oauth:grant-type:token-exchange` on `POST /oauth/token`, with
191
+ `subject_token_type`
192
+ `urn:collective-intelligence:params:oauth:token-type:session_token`, `scope`
193
+ and `organization_id`.
194
+
138
195
  ## Reuse Nest operations and local AI tools
139
196
 
140
197
  Use Node 22+ with ESM for the AI SDK 7 adapter. The inspected and tested baseline on 6 October 2026
@@ -359,8 +416,7 @@ succeeded before the new turn started count. A turn is
359
416
  retried message replay the same turn instead of adding one. Use each key for
360
417
  one message only; reusing it for a different message or conversation returns
361
418
  `conflict`.
362
- - The application's Agent capability gates conversations, with the `ai:invoke`
363
- scope.
419
+ - The `ai:invoke` scope gates conversations.
364
420
  - The Agent's current audience gates every turn and every `history` read. When
365
421
  an administrator removes the caller from the audience, the same caller gets
366
422
  `not_found` for its own conversations too.
@@ -371,6 +427,94 @@ succeeded before the new turn started count. A turn is
371
427
  subject, never with the application's scopes. Visitors are separate owners:
372
428
  one visitor never reads another visitor's conversation.
373
429
 
430
+ ## Member users
431
+
432
+ A confidential application administers the `member` users of its own
433
+ Organization: people whose only role there is `member` (Organization identity
434
+ and their own Knowledge; no CI Studio or dashboard). The Organization is the
435
+ one the credentials were authorized for through `organizationId`; a request
436
+ cannot reach another Organization.
437
+
438
+ ```ts
439
+ const ci = createServerClient({
440
+ authentication: { credentials, mode: "service" },
441
+ tenantUrl
442
+ })
443
+
444
+ const created = await ci.members.createOrGet({
445
+ email: "info@rossi.example",
446
+ name: "Rossi Srl",
447
+ organizationId
448
+ })
449
+ // created.value.result: { userId, email, name, outcome }
450
+ ```
451
+
452
+ - `createOrGet` resolves an email to one member user. `outcome` is `created`
453
+ for a new CI user, `linked` when an existing CI user joined the Organization
454
+ as a member, and `existing` when the user already was one. The email is
455
+ matched ignoring letter case, and concurrent or retried calls return the
456
+ same `userId`. It never sends CI's activation invitation and never accepts
457
+ a pending Organization invitation for the email; the application owns
458
+ onboarding.
459
+ - `get({ userId })`, `list({ page?, pageSize? })` (at most 100 per page),
460
+ `update({ userId, name })` and `remove({ userId })` act only on member users.
461
+ `update` changes the name only of a user who belongs to no other
462
+ Organization. `remove` ends the `member` membership and keeps the CI user.
463
+ - A user who holds any other role in the Organization (an administrator, for
464
+ example) is refused with `conflict` by every operation. `get`, `update` and
465
+ `remove` answer `not_found` for a user who is not in the Organization.
466
+ - Reads need the `members:read` scope and writes `members:write`. Only service
467
+ credentials may call them: a delegated user token or an anonymous visitor
468
+ gets `operation_not_allowed`.
469
+ - Only `temporarily_unavailable` is retryable (the client retries it); a
470
+ creation CI refuses is a non-retryable `internal_error`.
471
+
472
+ ## Member company links
473
+
474
+ An application links a member of its Organization to the Knowledge company
475
+ entities the member acts for. A member can have several companies and a company
476
+ several members.
477
+
478
+ ```ts
479
+ const linked = await ci.users.linkCompanies({
480
+ entityIds: [companyEntityId],
481
+ organizationId,
482
+ userId
483
+ })
484
+ // linked.value.result: { entityIds } — all of the user's links
485
+
486
+ const links = await ci.users.listCompanyLinks({ organizationId, userId })
487
+ ```
488
+
489
+ - Only confidential service credentials may call these operations: a delegated
490
+ user token or an anonymous visitor gets `operation_not_allowed`, and the
491
+ browser client and user proxy do not offer them.
492
+ - The Organization is the caller's. A user without membership in it, or an
493
+ entity that is missing, deleted, or in another Organization, returns
494
+ `not_found`; a refused call links nothing.
495
+ - Linking keeps existing links, so a retried call converges. Results list the
496
+ user's linked entities in entity id order.
497
+ - `knowledge:write` gates `linkCompanies`; `knowledge:read` gates
498
+ `listCompanyLinks`.
499
+ - CI accepts any entity of the Organization and does not check its schema; the
500
+ calling application decides which entities count as companies.
501
+ - Entity deduplication moves links to the surviving entity; physically deleting
502
+ an entity removes its links.
503
+ - A delegated user's `/application/v1/resources` and `knowledge_*` calls, and
504
+ the Agents, Skills and workflows the user runs, carry the user's links to
505
+ Knowledge policy as `subject.linkedCompanyIds`, and a policy reads the
506
+ entity id as `resource` path `["id"]`. To keep `member`-role users to their
507
+ own companies, declare a `deny` `filter` policy on the company schema: subject
508
+ match `user.role` equals `member`, rule "entity id not in
509
+ `linkedCompanyIds`". Gets and writes of any other company, creates included,
510
+ then fail with `forbidden` and lists omit it; other roles and service
511
+ credentials are unaffected.
512
+ - The policy binds an execution only while it acts as the member. An Agent or
513
+ Skill tool bound to a fixed user, or a workflow that runs as another user or
514
+ a service principal, decides as that principal, not as the member who
515
+ invoked it. Do not give Agents or workflows that members can reach a
516
+ fixed-user or run-as identity with wider Knowledge access.
517
+
374
518
  ## Gateway registry and orchestration
375
519
 
376
520
  Build one registry with `defineGateway`. Its `knowledge`, `datasets`, and
@@ -885,7 +1029,7 @@ const handle = createResourceHttpHandler({
885
1029
 
886
1030
  `auth` is the existing `createFederatedAuth(...)` instance. Its server-only
887
1031
  `getEntityContext` resolves current CI application, user, organization, scopes,
888
- roles and team IDs; browser session JSON remains identity and expiry only.
1032
+ roles and group IDs; browser session JSON remains identity and expiry only.
889
1033
  Missing CI policy context denies entity access. Local Better Auth sessions do
890
1034
  not establish CI entity authority. Browser framework adapters require this
891
1035
  verified context instead of boolean authorization. Machine gateway calls retain
@@ -965,6 +1109,36 @@ browser-visible HTTPS application origin, CI issuer, OAuth client, organization,
965
1109
  audience, scopes, and an application-owned atomic session store. The facade owns
966
1110
  PKCE login, callback, session inspection and logout under `/api/auth` by default.
967
1111
 
1112
+ `applicationOrigin` accepts one HTTPS origin or a list. Use a list when one
1113
+ server answers on several domains with one CI Client ID:
1114
+
1115
+ ```ts
1116
+ const auth = createFederatedAuth({
1117
+ applicationOrigin: ["https://app.acme.com", "https://app.acme.it"],
1118
+ audience,
1119
+ clientId,
1120
+ issuer,
1121
+ organizationId,
1122
+ scopes: ["knowledge:read"],
1123
+ session: { kind: "sealed_cookie", secrets }
1124
+ })
1125
+ ```
1126
+
1127
+ Each request uses its own URL origin. A request on an unlisted origin gets
1128
+ `400 invalid_request`, and session lookups on it return `null`. The `Origin`
1129
+ header, `returnTo` and logout checks compare with that request origin. Login
1130
+ and the token exchange both send `redirect_uri = <origin><basePath>/callback`.
1131
+ The pending login lives in a host-only cookie, so the callback arrives on the
1132
+ origin that started the login. The browser cannot choose the callback through a
1133
+ query parameter or header.
1134
+
1135
+ On the CI application, register every callback, for example
1136
+ `https://app.acme.com/api/auth/callback` and
1137
+ `https://app.acme.it/api/auth/callback`, with `ci apps redirect-uris add`.
1138
+ Add every origin as a trusted website with `ci apps origins add`. CI binds the
1139
+ authorization code to its `redirect_uri`, so an unregistered callback fails at
1140
+ authorization.
1141
+
968
1142
  The store persists only hashed opaque-cookie state, token grants and revisions;
969
1143
  the browser receives a `Secure`, `HttpOnly`, `SameSite=Lax` host cookie. Every
970
1144
  session lookup revalidates the short-lived CI identity. Revoked or expired CI