@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 +181 -7
- package/dist/admin.d.ts +451 -257
- package/dist/admin.js +2 -2
- package/dist/auth/better-auth.d.ts +2 -2
- package/dist/auth/federated.d.ts +7 -4
- package/dist/auth/federated.js +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/gateway.js +1 -1
- package/dist/index.d.ts +156 -8
- package/dist/index.js +10 -3
- package/dist/persistence/drizzle.d.ts +1 -1
- package/dist/persistence/drizzle.js +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +1 -1
- package/dist/runtime/effect.d.ts +64 -8
- package/dist/runtime/effect.js +1 -1
- package/dist/runtime/nestjs.d.ts +1 -1
- package/dist/runtime/next.d.ts +2 -2
- package/dist/runtime/web.d.ts +2 -2
- package/dist/tools/ai-sdk.js +1 -1
- package/package.json +1 -1
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
|
|
31
|
-
|
|
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 `
|
|
49
|
-
(`POST /admin/organization/
|
|
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
|
|
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
|
|
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
|