@collective-intelligence/external-apps-server-sdk 0.1.3 → 0.1.5

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
@@ -427,6 +427,54 @@ succeeded before the new turn started count. A turn is
427
427
  subject, never with the application's scopes. Visitors are separate owners:
428
428
  one visitor never reads another visitor's conversation.
429
429
 
430
+ ## Messages
431
+
432
+ `messages.send` sends one message through a messaging connection of the
433
+ Organization, for example a Telegram bot or a WhatsApp Business Account. The
434
+ application needs the `messaging:send` scope, and an administrator must grant
435
+ the connection to the application on its administration page.
436
+
437
+ ```ts
438
+ const sent = await ci.messages.send({
439
+ connectionId,
440
+ content: { kind: "text", text: "Your order is ready." },
441
+ idempotencyKey: `order-${orderId}-ready`,
442
+ organizationId,
443
+ target: { conversation, mode: "continue" }
444
+ })
445
+ // sent.value.result: { conversation, messageId }
446
+ ```
447
+
448
+ - `target` continues a conversation with the Conversation Reference of an
449
+ earlier message (`{ mode: "continue", conversation }`), or starts one with
450
+ the recipient fields of the vendor (`{ mode: "start_new", recipient }`).
451
+ - `content` is a text, or `{ kind: "voice", text, voice }` for a text that CI
452
+ speaks.
453
+ - `sender` holds the sender fields of the vendor, for example
454
+ `{ phoneNumberId }`. It is required when the account has more than one
455
+ sender. `service` names the vendor service when the connection has more
456
+ than one.
457
+ - The result is the Message Receipt: the message id of the vendor and the
458
+ Conversation Reference that continues the conversation. It means that the
459
+ vendor accepted the message, not that a person received it.
460
+ - `idempotencyKey` (at most 256 characters; generated when omitted) makes a
461
+ repeat safe. A repeat with the same key and content returns the first
462
+ outcome and sends nothing. A repeat with the same key and other content
463
+ returns `conflict`. Use your own key when your server can repeat a send
464
+ after a crash.
465
+ - CI sends once and never retries a failed send. A failed send returns
466
+ `message_send_failed` with a `sendErrorCode`, for example `window_closed`,
467
+ `recipient_unreachable`, `template_required` or `transport`. The same key
468
+ returns the same failure: use a new key to send again.
469
+ - `send_outcome_unknown` means that the first send with the key has no
470
+ recorded outcome: it still runs, or CI stopped before it recorded the
471
+ outcome. CI does not send again for the key. Repeat the call later; if the
472
+ answer stays the same, check with the vendor before you send with a new key.
473
+ - `not_found` means that the connection does not exist or is not granted to
474
+ the application. `rate_limited` (HTTP 429) means that the connection
475
+ reached its rate limit; it is retryable, and the client repeats it with the
476
+ same key.
477
+
430
478
  ## Member users
431
479
 
432
480
  A confidential application administers the `member` users of its own
@@ -503,11 +551,18 @@ const links = await ci.users.listCompanyLinks({ organizationId, userId })
503
551
  - A delegated user's `/application/v1/resources` and `knowledge_*` calls, and
504
552
  the Agents, Skills and workflows the user runs, carry the user's links to
505
553
  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
554
+ entity id as `resource` path `["id"]`. CI also sets `subject.access`:
555
+ `"read"` for gets, lists, searches, facets, autocomplete, traversal and
556
+ retrieval, `"write"` for creates, updates, deletes and relation writes. The
557
+ operation sets it; a caller cannot. A policy that does not test `access`
558
+ applies to reads and writes alike.
559
+ - To let `member`-role users write only their own companies but read every
560
+ company (for example through member-run Agents), declare a `deny` `filter`
561
+ policy on the company schema: subject match `user.role` equals `member` and
562
+ `access` equals `write`, rule "entity id not in `linkedCompanyIds`". Updates
563
+ and deletes of any other company, and creates, then fail with `forbidden`;
564
+ gets, lists and searches still return it. Without the `access` clause the
565
+ same policy also hides other companies from reads. Other roles and service
511
566
  credentials are unaffected.
512
567
  - The policy binds an execution only while it acts as the member. An Agent or
513
568
  Skill tool bound to a fixed user, or a workflow that runs as another user or
@@ -1124,6 +1179,12 @@ const auth = createFederatedAuth({
1124
1179
  })
1125
1180
  ```
1126
1181
 
1182
+ For local development, `applicationOrigin` and `issuer` also accept an `http`
1183
+ origin whose host is exactly `localhost`, a `127.0.0.0/8` address, or `[::1]`,
1184
+ for example `http://localhost:3000`. On such an origin the session cookie is
1185
+ `ci-federated`, without the `__Host-` prefix and the `Secure` attribute, which
1186
+ browsers do not accept over plain HTTP. Any other `http` origin is rejected.
1187
+
1127
1188
  Each request uses its own URL origin. A request on an unlisted origin gets
1128
1189
  `400 invalid_request`, and session lookups on it return `null`. The `Origin`
1129
1190
  header, `returnTo` and logout checks compare with that request origin. Login
@@ -1145,6 +1206,89 @@ session lookup revalidates the short-lived CI identity. Revoked or expired CI
1145
1206
  access ends application authorization on the next validation. A confidential
1146
1207
  OAuth client may supply `clientSecret`; a public PKCE client omits it.
1147
1208
 
1209
+ ### Configure from the CLI enrollment
1210
+
1211
+ `ci apps enroll --env-file .env.local` writes the application's identity as
1212
+ `CI_ISSUER`, `CI_AUDIENCE`, `CI_APPLICATION_ID`, `CI_CLIENT_ID`,
1213
+ `CI_ORGANIZATION_ID`, `CI_SCOPES` and, for a confidential application,
1214
+ `CI_CLIENT_SECRET`. `readApplicationEnrollment` reads those variables, so the
1215
+ application names none of them. It throws a `TypeError` that lists every
1216
+ missing variable. Pass the same prefix as `--prefix` when it is not `CI_`.
1217
+ `ci apps env <application>` prints the same variables for an application that
1218
+ already exists, for example to copy them into a deployment pipeline; it leaves
1219
+ out `CI_CLIENT_SECRET`, which CI shows only once.
1220
+
1221
+ ```ts
1222
+ import {
1223
+ createFederatedAuth,
1224
+ readApplicationEnrollment
1225
+ } from "@collective-intelligence/external-apps-server-sdk/auth/federated"
1226
+
1227
+ const enrollment = readApplicationEnrollment(process.env)
1228
+ export const auth = createFederatedAuth({
1229
+ ...enrollment,
1230
+ applicationOrigin: "http://localhost:3000",
1231
+ session: { kind: "sealed_cookie", secrets: [sessionSecret] }
1232
+ })
1233
+ ```
1234
+
1235
+ Register `<applicationOrigin>/api/auth/callback` as the redirect URI:
1236
+ `ci apps enroll --redirect-uri http://localhost:3000/api/auth/callback ...`.
1237
+
1238
+ ### Read the session without a `Request`
1239
+
1240
+ `getSession` and `getAccessToken` take a `Request`. Next.js Server Components
1241
+ and Server Functions have headers only. `sessionRequest` builds the request
1242
+ those reads need: it carries only the cookie, on the application origin.
1243
+
1244
+ ```ts
1245
+ import { sessionRequest } from "@collective-intelligence/external-apps-server-sdk/auth/federated"
1246
+ import { headers } from "next/headers"
1247
+
1248
+ const accessToken = await auth.getAccessToken?.(
1249
+ sessionRequest(await headers(), applicationOrigin)
1250
+ )
1251
+ ```
1252
+
1253
+ Mount `auth.handler` as the `GET` and `POST` handlers of
1254
+ `app/api/auth/[...path]/route.ts`.
1255
+
1256
+ ### Verify the access token in your own API
1257
+
1258
+ When the application forwards the user's CI access token to a separate API as
1259
+ `Authorization: Bearer`, that API checks it with `createAccessTokenVerifier`.
1260
+
1261
+ ```ts
1262
+ import {
1263
+ createAccessTokenVerifier,
1264
+ FederatedAuthRejectedError,
1265
+ readApplicationEnrollment
1266
+ } from "@collective-intelligence/external-apps-server-sdk/auth/federated"
1267
+
1268
+ const tokens = createAccessTokenVerifier(readApplicationEnrollment(process.env))
1269
+
1270
+ const caller = await tokens.verify(bearerToken)
1271
+ // userId, organizationId, applicationId, clientId, scopes, sessionId, expiresAt
1272
+
1273
+ const identity = await tokens.identity(bearerToken, { signal: request.signal })
1274
+ // the same fields, and user { id, email, name }, roles, groupIds
1275
+ ```
1276
+
1277
+ - `verify` is offline against the issuer key set: ES256 signature, issuer,
1278
+ audience, lifetime, and the application, client and Organization you supply.
1279
+ It does not see a revocation before the token expires.
1280
+ - `identity` runs `verify`, then reads CI's current identity for the token and
1281
+ checks that it describes the same grant. Use it when the API needs the
1282
+ user's roles or groups, or must stop at once when CI revokes the token. It
1283
+ is one CI request for each call, with a 10-second limit; cache the result
1284
+ for as long as a role change may take to reach your API. Pass the `signal`
1285
+ of the request you serve: when it aborts, the CI request ends and the call
1286
+ rejects with the abort reason.
1287
+
1288
+ Both throw `FederatedAuthRejectedError` for a token that is not valid for this
1289
+ application; answer `401`. Both throw `ApplicationUserAuthUnavailableError`
1290
+ when CI does not answer; answer `503`.
1291
+
1148
1292
  ### Forwarding a bound entity to CI
1149
1293
 
1150
1294
  Import `defineCiEntityResource` from `/resources` and use it with the same
package/dist/admin.d.ts CHANGED
@@ -4965,6 +4965,7 @@ declare const createAdministrationApi: (client: Client$1) => {
4965
4965
  description: string | null;
4966
4966
  directChildCount: number;
4967
4967
  directMemberCount: number;
4968
+ fqn: string | null;
4968
4969
  id: string;
4969
4970
  inheritedMembershipImpact: {
4970
4971
  descendantGroupCount: number;
@@ -4973,6 +4974,11 @@ declare const createAdministrationApi: (client: Client$1) => {
4973
4974
  };
4974
4975
  name: string;
4975
4976
  organizationId: string;
4977
+ package: {
4978
+ fqn: string;
4979
+ managed: boolean;
4980
+ readonly: boolean;
4981
+ } | null;
4976
4982
  parentGroupId: string | null;
4977
4983
  parentPath: Array<{
4978
4984
  id: string;
@@ -5001,6 +5007,7 @@ declare const createAdministrationApi: (client: Client$1) => {
5001
5007
  description: string | null;
5002
5008
  directChildCount: number;
5003
5009
  directMemberCount: number;
5010
+ fqn: string | null;
5004
5011
  id: string;
5005
5012
  inheritedMembershipImpact: {
5006
5013
  descendantGroupCount: number;
@@ -5009,6 +5016,11 @@ declare const createAdministrationApi: (client: Client$1) => {
5009
5016
  };
5010
5017
  name: string;
5011
5018
  organizationId: string;
5019
+ package: {
5020
+ fqn: string;
5021
+ managed: boolean;
5022
+ readonly: boolean;
5023
+ } | null;
5012
5024
  parentGroupId: string | null;
5013
5025
  parentPath: Array<{
5014
5026
  id: string;
@@ -57,6 +57,67 @@ export type UserAuthScopePolicy = Readonly<{
57
57
  writeScope: string;
58
58
  }>;
59
59
  export type UserAuthAuthorization = (resourceId: string) => (request: Request, operation?: string) => Promise<EntityInvocationContext | Response>;
60
+ export type CiFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
61
+ export type AccessTokenVerifierOptions = Readonly<{
62
+ /** CI External App id (`app_id` claim). Give this, `clientId`, or both. */
63
+ applicationId?: string;
64
+ audience: string;
65
+ /** OAuth client id (`client_id` claim). Give this, `applicationId`, or both. */
66
+ clientId?: string;
67
+ fetch?: CiFetch;
68
+ /** HTTPS CI issuer, or a loopback HTTP issuer for local development. */
69
+ issuer: string;
70
+ now?: () => number;
71
+ /** When set, a token of another Organization is rejected. */
72
+ organizationId?: string;
73
+ }>;
74
+ export type VerifiedAccessToken = Readonly<{
75
+ applicationId: string;
76
+ clientId: string;
77
+ expiresAt: Date;
78
+ organizationId: string;
79
+ scopes: readonly string[];
80
+ sessionId: string;
81
+ userId: string;
82
+ }>;
83
+ export type AccessTokenIdentity = VerifiedAccessToken & Readonly<{
84
+ /** Absent when CI supplies no current group policy for the application. */
85
+ groupIds: readonly string[] | undefined;
86
+ /** CI Organization roles; absent when CI supplies no current role policy. */
87
+ roles: readonly string[] | undefined;
88
+ user: Readonly<{
89
+ email: string | null;
90
+ id: string;
91
+ name: string | null;
92
+ }>;
93
+ }>;
94
+ export type AccessTokenVerifier = Readonly<{
95
+ /**
96
+ * The token's current CI identity: `verify`, then CI's own answer for the
97
+ * user, roles and groups. A token CI has revoked is rejected. Pass the
98
+ * `signal` of the request being served: when it aborts, the CI request ends
99
+ * and the call rejects with the abort reason.
100
+ */
101
+ identity: (token: string, options?: Readonly<{
102
+ signal?: AbortSignal;
103
+ }>) => Promise<AccessTokenIdentity>;
104
+ /**
105
+ * Offline check against the issuer key set. It does not see a revocation
106
+ * before the token expires.
107
+ */
108
+ verify: (token: string) => Promise<VerifiedAccessToken>;
109
+ }>;
110
+ /** The application's CI enrollment, as `ci apps enroll --env-file` writes it. */
111
+ export type ApplicationEnrollment = Readonly<{
112
+ applicationId: string;
113
+ audience: string;
114
+ clientId: string;
115
+ /** Present for a confidential application only. */
116
+ clientSecret?: string;
117
+ issuer: string;
118
+ organizationId: string;
119
+ scopes: readonly string[];
120
+ }>;
60
121
  declare class FederatedAuthRejectedError extends Error {
61
122
  readonly code: "invalid_request" | "invalid_state" | "invalid_identity" | "authorization_failed";
62
123
  constructor(code: FederatedAuthRejectedError["code"]);
@@ -104,6 +165,8 @@ export type FederatedAuthOptions = Readonly<{
104
165
  /**
105
166
  * HTTPS browser-visible origin, or list of origins, that owns the auth routes and __Host cookie.
106
167
  * Each request uses its own listed origin; register `<origin><basePath>/callback` for each one.
168
+ * A loopback HTTP origin such as `http://localhost:3000` is accepted for local development;
169
+ * its cookie has no `__Host-` prefix and no `Secure` attribute.
107
170
  */
108
171
  applicationOrigin: string | readonly string[];
109
172
  audience: string;
@@ -125,8 +188,11 @@ declare const FederatedAuthRejectedError$1: typeof FederatedAuthRejectedError;
125
188
  type FederatedAuthRejectedError$1 = FederatedAuthRejectedError;
126
189
  declare const ApplicationUserAuthUnavailableError$2: typeof ApplicationUserAuthUnavailableError;
127
190
  export declare const authorizeWithUserAuth: (auth: Pick<ApplicationUserAuth, "getEntityContext">, policy: UserAuthScopePolicy) => UserAuthAuthorization;
191
+ export declare const createAccessTokenVerifier: (options: AccessTokenVerifierOptions) => AccessTokenVerifier;
128
192
  export declare const createFederatedAuth: (options: FederatedAuthOptions) => ApplicationUserAuth;
129
193
  declare const FederatedAuthRejectedError$2: typeof FederatedAuthRejectedError;
194
+ export declare const readApplicationEnrollment: (env: Readonly<Record<string, string | undefined>>, prefix?: string) => ApplicationEnrollment;
195
+ export declare const sessionRequest: (headers: Headers, applicationOrigin: string) => Request;
130
196
  type ApplicationUserAuthUnavailableError$2 = ApplicationUserAuthUnavailableError$1;
131
197
  type FederatedAuthRejectedError$2 = FederatedAuthRejectedError$1;
132
198