@permute/sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -224,10 +224,36 @@ console.log(uploaded.sourceId, uploaded.status);
224
224
  ```
225
225
 
226
226
  The SDK infers `sizeBytes` for strings, buffers, typed arrays, ArrayBuffers, and
227
- Blobs. REST callers provide `sizeBytes`, then send a raw `PUT` to the returned
227
+ Blobs. Omit `data` and provide `sizeBytes` to create the dataset and return its
228
+ upload target without sending bytes. This lets an action approve upload metadata
229
+ while the browser uploads a file directly to storage (outside action limits):
230
+
231
+ ```ts
232
+ const prepared = await client.datasets.create({
233
+ type: 'file',
234
+ fileName: file.name,
235
+ contentType: file.type,
236
+ sizeBytes: file.size,
237
+ });
238
+ const upload = prepared.data.upload;
239
+ if (!upload) throw new Error('Missing upload target');
240
+ const response = await fetch(upload.url, {
241
+ method: upload.method,
242
+ headers: upload.headers,
243
+ body: file,
244
+ });
245
+ if (!response.ok) throw new Error('Upload failed');
246
+ ```
247
+
248
+ For browser uploads, prepare the target on the server or inside a Business Action,
249
+ then return the dataset ID and target to the browser. The storage PUT uses the
250
+ signed target without an API key; keep API-key SDK clients on the server.
251
+
252
+ Use every header returned with the target. REST callers provide `sizeBytes`, then send a raw `PUT` to the returned
228
253
  `data.upload.url` with every header in `data.upload.headers`. Poll
229
- `client.datasets.get(result.id)` for processing status; there is no completion
230
- API call.
254
+ `client.datasets.get(result.id)` for processing status with dataset read permission.
255
+ If the caller has only action invocation access, use a separate status action.
256
+ There is no completion API call.
231
257
 
232
258
  CSV, TSV, JSON, NDJSON, and Parquet files can be up to 1 GiB (1,073,741,824
233
259
  bytes). Other supported file types can be up to 100 MiB (104,857,600 bytes).
@@ -382,7 +408,11 @@ the same transaction after an uncertain response.
382
408
  Schema definitions use `client.databases.definition(id)` and
383
409
  `client.databases.saveDefinition(id, { definition, expectedDraft })`. Save the
384
410
  complete table list, then publish the exact draft through `client.revisions`.
385
- Published schemas only support adding tables; rows are not part of schema history.
411
+ Publication supports new tables and new nullable columns on existing tables.
412
+ Set `nullable: true` for added columns; existing rows read `NULL` and keep their
413
+ record-version tokens. Preserve existing columns, types, nullability and primary
414
+ keys. Rows are not part of schema history. Publication applies DDL before moving
415
+ the published ref; after a partial failure, retry the same definition to resume.
386
416
 
387
417
  **Upgrading from 0.3.x:** `createTable` now saves a draft. Publish the database
388
418
  before using the new table. Direct `transact` calls require database admin
@@ -425,7 +455,22 @@ const files = await client.apps.uploadFiles(app.id, [
425
455
  { path: 'index.html', data: await readFile('dist/index.html') },
426
456
  { path: 'assets/main.js', data: await readFile('dist/assets/main.js') },
427
457
  ]);
458
+ const sourceFiles = await client.apps.uploadFiles(app.id, [
459
+ { path: 'package.json', data: await readFile('package.json') },
460
+ { path: 'bun.lock', data: await readFile('bun.lock') },
461
+ { path: 'src/main.tsx', data: await readFile('src/main.tsx') },
462
+ { path: 'index.html', data: await readFile('index.html') },
463
+ { path: 'vite.config.ts', data: await readFile('vite.config.ts') },
464
+ ]);
428
465
  const saved = await client.apps.saveSite(app.id, {
466
+ source: {
467
+ files: sourceFiles,
468
+ build: {
469
+ command: 'bun run build --base=./',
470
+ outputDirectory: 'dist',
471
+ runtime: 'bun@1.3.12',
472
+ },
473
+ },
429
474
  site: { files, spa: true },
430
475
  expectedDraft: app.draft,
431
476
  });
@@ -436,40 +481,62 @@ if (state.draft?.revisionId !== saved.draft.revisionId || state.draft.sequence !
436
481
  await client.revisions.publish(app.id, { expectedVersion: state.version });
437
482
  ```
438
483
 
439
- Import `readFile` from `node:fs/promises`. Supply the complete build directory; the filenames above are illustrative. Uploads hash the bytes and use signed, conditional S3 PUTs without forwarding API credentials. Uploading does not create a draft. `saveSite` verifies the uploaded files and saves the complete manifest as a draft revision; `revisions.publish` makes that draft live. The limits are 200 files, 4 MiB per file, and 50 MiB total.
484
+ Import `readFile` from `node:fs/promises`. Supply complete source and compiled manifests, including every file required for a standalone build; the filenames above are illustrative. Uploads hash the bytes and use signed, conditional S3 PUTs without forwarding API credentials. Uploading does not create a draft. `saveSite` verifies the uploaded files and saves source, build instructions and compiled output as one draft revision; `revisions.publish` makes that draft live. Source allows 1,000 files; compiled output allows 200. Each manifest allows 4 MiB per file and 50 MiB total. Optional source-file `executable: true` preserves script permissions. Keep secrets outside source and browser bundles.
440
485
 
441
486
  Identical contents reuse existing files. Changed contents at the same path are stored separately, preserving earlier versions for rollback. Each save replaces the complete manifest (including unchanged files); omitted paths disappear from the live app when published. Resolve a 409 conflict against the current draft before retrying.
442
487
 
488
+ Customize login appearance with `apps.update(app.id, { login, expectedDraft: app.draft })`. The optional `login` object accepts `header`, `subheader`, `title`, `subtitle`, `primaryColor` and `secondaryColor` (six-digit hex colors). For `sidePanelImage`, upload a PNG, JPEG or WebP image up to 2 MiB with `apps.uploadFiles` and pass the returned file descriptor. Publish the saved draft through `revisions.publish` to apply the same appearance to Permute sign-in and shared-password access. Set `login: null` to restore default styling. Images displayed before sign-in are public; normal site files stay gated.
489
+
490
+ Set an app favicon with `apps.update(app.id, { favicon, expectedDraft: app.draft })`, where `favicon` is the descriptor returned by uploading a PNG up to 256 KiB through `apps.uploadFiles`. Publish to apply it to browser tabs and sign-in screens (independent of custom login styling). The configured icon overrides bundled icons; `favicon: null` removes the override. Only the current published icon is public before sign-in; historical published icons require an app session. Organization defaults are not supported.
491
+
443
492
  Visitors sign in through Permute and need app read permission. HTML receives a revision-specific base URL; use relative URLs for bundled and public assets. Inline scripts, external requests, workers, and server runtimes are not supported. Keep API keys and secrets out of compiled browser files. Restore a historical revision with `client.revisions.restore`, then publish the resulting draft to roll back. See the [complete app guide](https://permute.ai/docs/guides/deploy-app) for creation, directory uploads, updates, and rollback.
444
493
 
445
- ### Deploy a build directory (SDK 0.6.0)
494
+ ### Deploy source and compiled output (SDK 0.7.0)
446
495
 
447
496
  The Node/Bun entry `@permute/sdk/deploy` combines validation, upload, draft save,
448
- and publication for an existing live app. Use SDK 0.6.0 for the grant-based app
449
- API shown here.
497
+ and publication for an existing live app. Use SDK 0.7.0 for source-backed app saves. Builds still run locally.
450
498
 
451
499
  ```ts
452
500
  import { deployApp } from '@permute/sdk/deploy';
453
501
 
454
- const deployed = await deployApp(client, { appId, directory: './dist' });
502
+ const deployed = await deployApp(client, {
503
+ appId,
504
+ directory: './dist',
505
+ expectedDraft: app.draft,
506
+ source: {
507
+ directory: '.',
508
+ build: {
509
+ command: 'bun run build --base=./',
510
+ outputDirectory: 'dist',
511
+ runtime: 'bun@1.3.12',
512
+ },
513
+ },
514
+ });
455
515
  console.log(deployed.url, deployed.revisionId, deployed.changed);
456
516
  ```
457
517
 
458
518
  The helper rejects symbolic links, unsupported paths, missing `index.html`, and
459
- oversized builds before uploading. It requires the current draft to be published;
460
- after reviewing an existing draft, pass its exact `expectedDraft` to approve
461
- replacing it. Omitted `actions` preserve current bindings (an empty array clears them). To change action bindings, supply explicit published revisions:
519
+ oversized builds before uploading. Pass the `expectedDraft` token from checkout or a reviewed app read. A stale token stops deployment before uploads; the helper never adopts a newer remote draft as the base of local files. Omitted `actions` preserve current bindings (an empty array clears them). To change action bindings, supply explicit published revisions:
462
520
 
463
521
  ```ts
464
522
  await deployApp(client, {
465
523
  appId,
524
+ expectedDraft: app.draft,
466
525
  directory: './dist',
526
+ source: {
527
+ directory: '.',
528
+ build: {
529
+ command: 'bun run build --base=./',
530
+ outputDirectory: 'dist',
531
+ runtime: 'bun@1.3.12',
532
+ },
533
+ },
467
534
  actions: [{ actionId, revisionId: publishedActionRevisionId }],
468
535
  });
469
536
  ```
470
537
 
471
538
  An unchanged build returns `changed: false` without uploading, saving, or publishing
472
- (files, SPA mode, and any supplied bindings must match). It rechecks refs before
539
+ (source, build instructions, compiled files, SPA mode, and supplied bindings must match). It rechecks refs before
473
540
  reporting no change. An approved unpublished draft is still published so settings
474
541
  edits are preserved. Changed deployments return `changed: true`.
475
542
 
@@ -480,23 +547,100 @@ publication may already have completed. `deployment_conflict` requires review;
480
547
  when saving a draft for later review. The helper does not build, provision, or wait
481
548
  for hosting, and must not be imported into browser code.
482
549
 
550
+ ### Shared source checkpoints and checkout
551
+
552
+ Source and compiled output share one App history. Source checkpoints advance the draft without publishing; publication checks that the saved build matches current source and build instructions. Settings-only edits reuse matching builds. Restore recovers both manifests together; older compiled-only revisions have no downloadable source.
553
+
554
+ Invited editors can use **Download source** in App details with their Permute user session (no API key required). The ZIP contains project files under `source/` and a separate `app-source.json` with build instructions, the exact revision and the observed draft token. The SDK uses an API key with its own App write grant; a user editing invitation does not grant access to any API key.
555
+
556
+ ```ts
557
+ import { checkoutApp, saveAppSource } from '@permute/sdk/deploy';
558
+
559
+ const checkout = await checkoutApp(client, { appId, directory: './app-project' });
560
+ // Edit the downloaded files. Retain the token from checkout for your save.
561
+ if (!checkout.expectedDraft) throw new Error('Review the current draft first');
562
+ const saved = await saveAppSource(client, {
563
+ appId,
564
+ expectedDraft: checkout.expectedDraft,
565
+ source: { directory: './app-project', build: checkout.source.build },
566
+ });
567
+ // Run the stored build command, then use deployApp with expectedDraft: saved.draft.
568
+ ```
569
+
570
+ Checkout requires a new folder and verifies every downloaded file against its hash and size. Pass `revisionId` to select historical source; historical checkout returns no draft token unless it is also the current draft. A failed checkout may leave a partial folder; inspect it and choose a new folder before retrying.
571
+
572
+ Source helpers apply root and nested `.gitignore` rules, including directory patterns and `!` exceptions, using [`ignore` 7.0.5](https://github.com/kaelzhang/node-ignore/tree/7.0.5). Visited `.gitignore` files stay in the source snapshot. Rules apply to all local files, including files tracked by Git; the Git index, local/global excludes and rules outside the selected source root are not consulted. Git installation and commits are unnecessary.
573
+
574
+ Mandatory exclusions cover dependencies, credential files, common caches, the declared build output directory, and personal agent state (including `CLAUDE.local.md`, `.claude/settings.local.json`, agent worktrees, authentication files and session logs). Ignore exceptions cannot override these exclusions. Keep shared instructions, skills, setup scripts, lockfiles, tests and configuration inside the source root. Environment templates ending in `.example`, `.sample` or `.template` are allowed; remove actual secrets from their contents. Filename exclusions do not scan for secrets embedded in ordinary files.
575
+
576
+ Internal source symlinks are copied as ordinary files or directories, and checkout restores executable scripts. External, dangling, cyclic and excluded-target links fail before uploads; compiled output rejects symlinks. Unsafe paths, case collisions and oversized manifests also fail. Checkout never executes project hooks or scripts. Build the exact source you intend to save and keep it unchanged while deploying; build pairing records the uploader’s declaration and does not independently verify compilation.
577
+
578
+ App write/admin permission is required for source files and complete revision reads. App admins can grant editing access through Share; app read permission continues to govern hosted compiled files. Source files never enter hosted-file delivery.
579
+
483
580
  ### Inside a hosted app
484
581
 
485
582
  Use the browser entry with the app's existing sign-in session (no API key in frontend code):
486
583
 
584
+ Use `app.sources.list()`, `app.sources.get(sourceId)`, and `app.query()` for ordinary
585
+ reads, including counts, lists, filters, and aggregates. Create read actions only
586
+ for deliberate delegated contracts, reusable business rules, or live connector
587
+ calls outside the query API. The built-in assistant queries permitted sources
588
+ directly; invoking any bound action requires viewer confirmation.
589
+
590
+ Source reads require whole-resource read permission for both the app and viewer,
591
+ including databases, datasets, connectors, and operation outputs. Table, row,
592
+ and column grants are unsupported; subResourceId is available only to revoke an
593
+ existing scoped grant. Test source discovery and
594
+ a bounded query as the intended viewer before testing the assistant; an empty
595
+ source list describes that viewer's app access, not the workspace's contents.
596
+
597
+ Plan app and viewer permissions separately. Use `client.apps.setGrant()` for exact
598
+ approved app source IDs; an administrator uses Permute sharing for viewer app and
599
+ source access. The public SDK has no general user-permission mutation methods.
600
+ Inspect the complete source before approval (a database may contain other apps’
601
+ tables); never widen a rejected table grant automatically. Apps and keys do not
602
+ inherit their creators’ access. The public
603
+ [app guide](https://permute.ai/docs/guides/deploy-app#app-grants) includes the permission matrix.
604
+
605
+ Use the built-in assistant rather than adding another AI backend or chat overlay.
606
+ When migrating ordinary read actions, update their callers to the source/query API,
607
+ publish without those bindings, revoke their app grants, and delete the actions
608
+ after confirming no remaining callers. Keep approved write actions and revision pins.
609
+
487
610
  ```ts
488
611
  import { PermuteAppClient } from '@permute/sdk/app';
489
612
 
490
613
  const app = new PermuteAppClient();
614
+ const user = await app.me(); // { id, name, email } for the current signed-in viewer
491
615
  const result = await app.query({
492
616
  sourceIds: ['dbas_example'],
493
617
  sql: 'SELECT * FROM app.contacts',
494
618
  limit: 100,
495
619
  });
620
+ // Optional: only for an explicitly requested operation with a pinned action.
496
621
  const request = { requestId: crypto.randomUUID(), input: { name: 'Ada' } };
497
622
  const receipt = await app.actions.invoke('actn_example', request);
498
623
  ```
499
624
 
625
+ `app.me()` returns the current user’s `id`, `name` and `email`, after checking the app session, membership and app access. Requests use the public API on the app origin (including custom domains), with the HttpOnly cookie and a validated `X-Permute-App-Revision` header. Source reads and lists intersect current user permissions and app grants; action invocation retains the served release’s pinned binding. Password demos cannot call these APIs.
626
+
627
+ ```ts
628
+ const sources = await app.sources.list();
629
+ const chat = await app.chats.create({
630
+ action: 'send_message',
631
+ clientRequestId: crypto.randomUUID(),
632
+ message: 'Show the pipeline',
633
+ });
634
+ const state = await app.chats.get(chat.id);
635
+ await app.chats.send(chat.id, {
636
+ action: 'send_message',
637
+ clientRequestId: crypto.randomUUID(),
638
+ message: 'Group it by stage',
639
+ });
640
+ ```
641
+
642
+ `/v1/chats` preserves conversation and supports turns, cancellation and action decisions. `/v1/answer-jobs` remains one-shot analysis. Chat and basic-profile routes require user authentication; API keys cannot own chats. Other API routes retain their declared authentication requirements. Legacy hosted `__me`, `__queries` and `__actions/{actionId}/invoke` URLs forward to these public handlers. Port older apps by updating the SDK, rebuilding and publishing their bundles so these aliases can be removed. Existing users must sign in again when migrating from opaque cookies to app-scoped JWTs.
643
+
500
644
  The client reads the hosted page's revision-pinned base URL. Save
501
645
  `actions: [{ actionId, revisionId }]` with its site manifest, then publish the app revision.
502
646
  Queries require current app and user read permissions. The query request selects its
@@ -520,13 +664,51 @@ operation only after resolving any earlier uncertain write.
520
664
 
521
665
  ## Business actions
522
666
 
667
+ Action handlers can receive a declared `platform` SDK facade. Add `platform.methods`
668
+ and `platform.permissions` to the definition, then call
669
+ `platform.withStep('upload').datasets.create({ type: 'file', fileName: input.fileName, contentType: input.contentType, sizeBytes: input.sizeBytes })`.
670
+ Without `data`, the call returns dataset metadata and an upload target; the handler
671
+ can return that target for the browser to upload bytes. Providing `data` also
672
+ uploads bytes inside the action and must fit its execution and payload limits.
673
+ The server runs the real SDK using the action's delegated grants (callers need
674
+ only action invocation permission). Saving and publishing require resource admin
675
+ authority. Every SDK call needs a distinct, deterministic step ID, including reads.
676
+ Completed calls replay their saved results; reusing an ID with a different method
677
+ or arguments returns `request_conflict`. Calls with uncertain outcomes return
678
+ `outcome_unknown` and block automatic resend. Earlier SDK effects aren't rolled
679
+ back if the action fails.
680
+ Calls share action time and payload limits; dataset helpers return plain data.
681
+ See the [business-actions guide](https://permute.ai/docs/guides/business-actions)
682
+ for supported methods, limits, and authoring examples.
683
+
523
684
  Use `client.actions.create`, `definition`, and `saveDefinition` to author a typed
524
- handler with selected database tables or connector accounts. Publish with
685
+ handler with declared database, connector, or SDK capabilities. SDK-only and
686
+ connector-only actions need no managed database. Publish with
525
687
  `client.revisions.publish`, then call `client.actions.invoke` with a stable
526
688
  `requestId` and input. `list` exposes published contracts; reading definitions
527
689
  requires admin permission. See the [business action guide](https://permute.ai/docs/guides/business-actions)
528
690
  for the complete create, save, publish, and invoke workflow.
529
691
 
692
+ Permute authenticates callers and checks the action’s required invocation
693
+ permission. Hosted app calls also require app access, an app invocation grant,
694
+ and the action revision pinned in the served release. Administrators approve
695
+ delegated resources when authoring and publishing the action.
696
+
697
+ Execution uses the action’s approved delegated capabilities; callers do not need
698
+ direct access to the underlying sources. A bound connector uses the connected
699
+ account’s access even when the caller has no connector access. Permute enforces
700
+ declared execution capabilities and validates input/output schemas; the handler
701
+ defines the operation exposed to callers.
702
+
703
+ Handlers receive trusted `caller: { id, type }` (`type` is `user` or `apiKey`).
704
+ An action can use that identity and server-side business data to check or deny a
705
+ request when its rules warrant it (such as an ownership rule or allowed state transition).
706
+ Apply those checks before the affected reads or writes. They supplement the
707
+ platform checks. Permute does not require callers to hold the action’s underlying
708
+ source permissions. Browser-supplied actor IDs or roles are not proof of identity
709
+ or authority. The caller object contains no membership roles or general
710
+ permission lookup API.
711
+
530
712
  ### Expected failures in handlers
531
713
 
532
714
  Use `reject(code, message, details?)` for a deliberate business rejection:
@@ -540,7 +722,7 @@ exports.handler = ({ input, db, reject }) => {
540
722
  ```
541
723
 
542
724
  Rejection stops the handler and discards staged database writes. It cannot undo
543
- connector calls already sent. Messages and details are public to callers; never
725
+ connector or SDK effects already sent. Messages and details are public to callers; never
544
726
  put credentials or private diagnostic data in them. A rejection message permits
545
727
  1–1,000 characters, with up to 20 string detail fields (keys 1–100 characters,
546
728
  values up to 500). Invalid rejections and unexpected exceptions return a safe
@@ -626,3 +808,31 @@ step IDs stable in code. The first attempt pins the revision; successful externa
626
808
  replay saved responses. Steps with uncertain outcomes block automatic resend and require
627
809
  provider-state reconciliation. There is no automatic rollback, reconciliation API, or
628
810
  long-running workflow runner. Connector-only actions can use `sources: []`.
811
+
812
+ ## Hosted app visitor access (next release)
813
+
814
+ App admins can read `client.apps.access(appId)` and update access with
815
+ `client.apps.setAccess(appId, { mode, expectedVersion, password? })`. Use the
816
+ version returned by the read. `mode: 'password'` requires a new password on each
817
+ save; `mode: 'permute'` clears it. These methods are not in SDK 0.6.0.
818
+
819
+ Access changes take effect immediately and invalidate app sessions (without
820
+ publishing a site revision). Shared passwords gate published frontend files only,
821
+ including older releases. Use mocked data; demo sessions cannot query sources or
822
+ invoke Business Actions. Permute sign-in retains existing permissions and app grants.
823
+
824
+ ### Custom app addresses
825
+
826
+ An organization admin first connects a namespace in Organization Settings → App domains and completes the displayed DNS validation. App admins can then assign a name without a new build or CloudFront rollout (the generated Permute URL stays available):
827
+
828
+ ```ts
829
+ const current = await client.apps.address(app.id);
830
+ const namespace = current.namespaces[0]; // Ready namespaces in this organization.
831
+ if (!namespace) throw new Error('Connect an organization app domain first.');
832
+ await client.apps.setAddress(app.id, {
833
+ address: { namespaceId: namespace.id, label: 'crm' },
834
+ expectedUrl: current.address?.url ?? null,
835
+ });
836
+ ```
837
+
838
+ Set `address: null` with the currently observed URL to remove it. Stale edits and occupied names return 409. Each hostname uses its own app session; permissions and password-demo restrictions apply on every address. Namespace ownership management is restricted to signed-in organization admins, while these app-level methods support API keys with app admin permission.
package/dist/app.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse } from './types.js';
1
+ import type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse, AppUser, ChatSendRequest, ChatDecisionRequest, ChatResponse, SourceList, SourceDetail } from './types.js';
2
2
  export { PermuteApiError } from './errors.js';
3
- export type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse } from './types.js';
3
+ export type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse, AppUser, ChatSendRequest, ChatDecisionRequest, ChatResponse, SourceList, SourceDetail, } from './types.js';
4
4
  export interface PermuteAppClientOptions {
5
5
  /** Defaults to the hosted page's revision-pinned document.baseURI. */
6
6
  baseUrl?: string;
@@ -12,12 +12,26 @@ export interface AppRequestOptions {
12
12
  /** Browser client authenticated by the hosted app's HttpOnly session cookie. */
13
13
  export declare class PermuteAppClient {
14
14
  private readonly baseUrl;
15
+ private readonly revisionId;
15
16
  private readonly fetcher;
16
17
  constructor(options?: PermuteAppClientOptions);
17
18
  readonly actions: {
18
19
  /** Keep requestId and input unchanged when retrying an uncertain result. */
19
20
  invoke: (actionId: string, request: ActionRequest, options?: AppRequestOptions) => Promise<ActionReceipt>;
20
21
  };
22
+ readonly sources: {
23
+ list: (options?: AppRequestOptions) => Promise<SourceList>;
24
+ get: (sourceId: string, options?: AppRequestOptions) => Promise<SourceDetail>;
25
+ };
26
+ readonly chats: {
27
+ create: (request: ChatSendRequest, options?: AppRequestOptions) => Promise<ChatResponse>;
28
+ get: (chatId: string, options?: AppRequestOptions) => Promise<ChatResponse>;
29
+ send: (chatId: string, request: ChatSendRequest, options?: AppRequestOptions) => Promise<ChatResponse>;
30
+ stop: (chatId: string, options?: AppRequestOptions) => Promise<ChatResponse>;
31
+ decide: (chatId: string, request: ChatDecisionRequest, options?: AppRequestOptions) => Promise<ChatResponse>;
32
+ };
21
33
  query(request: QueryRequest, options?: AppRequestOptions): Promise<QueryResponse>;
34
+ /** Basic profile of the current Permute user. Password demos have no user identity. */
35
+ me(options?: AppRequestOptions): Promise<AppUser>;
22
36
  private request;
23
37
  }
package/dist/app.js CHANGED
@@ -3,6 +3,7 @@ export { PermuteApiError } from './errors.js';
3
3
  /** Browser client authenticated by the hosted app's HttpOnly session cookie. */
4
4
  export class PermuteAppClient {
5
5
  baseUrl;
6
+ revisionId;
6
7
  fetcher;
7
8
  constructor(options = {}) {
8
9
  const base = options.baseUrl ?? (typeof document !== 'undefined' ? document.baseURI : undefined);
@@ -14,23 +15,42 @@ export class PermuteAppClient {
14
15
  throw new Error('Use a revision-pinned hosted app URL.');
15
16
  if (typeof location !== 'undefined' && url.origin !== location.origin)
16
17
  throw new Error('App requests must use the current origin.');
17
- this.baseUrl = `${url.origin}${revision[0]}`;
18
+ this.baseUrl = `${url.origin}/v1/`;
19
+ this.revisionId = revision[1];
18
20
  this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
19
21
  }
20
22
  actions = {
21
23
  /** Keep requestId and input unchanged when retrying an uncertain result. */
22
- invoke: (actionId, request, options) => this.request(`__actions/${encodeURIComponent(actionId)}/invoke`, request, options),
24
+ invoke: (actionId, request, options) => this.request(`actions/${encodeURIComponent(actionId)}/invoke`, request, options),
25
+ };
26
+ sources = {
27
+ list: (options) => this.request('sources', undefined, options),
28
+ get: (sourceId, options) => this.request(`sources/${encodeURIComponent(sourceId)}`, undefined, options),
29
+ };
30
+ chats = {
31
+ create: (request, options) => this.request('chats', request, options),
32
+ get: (chatId, options) => this.request(`chats/${encodeURIComponent(chatId)}`, undefined, options),
33
+ send: (chatId, request, options) => this.request(`chats/${encodeURIComponent(chatId)}/turns`, request, options),
34
+ stop: (chatId, options) => this.request(`chats/${encodeURIComponent(chatId)}/stop`, {}, options),
35
+ decide: (chatId, request, options) => this.request(`chats/${encodeURIComponent(chatId)}/decisions`, request, options),
23
36
  };
24
37
  query(request, options) {
25
- return this.request('__queries', request, options);
38
+ return this.request('queries', request, options);
39
+ }
40
+ /** Basic profile of the current Permute user. Password demos have no user identity. */
41
+ me(options) {
42
+ return this.request('me', undefined, options);
26
43
  }
27
44
  async request(path, body, options) {
28
45
  const response = await this.fetcher(`${this.baseUrl}${path}`, {
29
- method: 'POST',
46
+ method: body === undefined ? 'GET' : 'POST',
30
47
  credentials: 'same-origin',
31
48
  mode: 'same-origin',
32
49
  redirect: 'error',
33
- headers: { 'Content-Type': 'application/json' },
50
+ headers: {
51
+ 'X-Permute-App-Revision': this.revisionId,
52
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
53
+ },
34
54
  body: JSON.stringify(body),
35
55
  signal: options?.signal,
36
56
  });
@@ -0,0 +1,10 @@
1
+ import { PermuteClient } from './client.js';
2
+ import type { PublicApiOperationId } from './generated/runtime-contract.js';
3
+ /** Executes the SDK itself behind a JSON RPC boundary; it does not implement API operations. */
4
+ export declare class PermuteSdkBridge {
5
+ private readonly client;
6
+ constructor(client: PermuteClient);
7
+ supports(method: string, acceptsOperation?: (operationId: PublicApiOperationId) => boolean): boolean;
8
+ invoke(method: string, args: unknown[]): Promise<unknown>;
9
+ private resolve;
10
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,62 @@
1
+ import { PermuteDataset, PermuteFileDataset } from './client.js';
2
+ import { SdkMethodFactory } from './method.js';
3
+ const safeName = (name) => /^[A-Za-z][A-Za-z0-9_]*$/.test(name) && !['constructor', 'prototype', '__proto__'].includes(name);
4
+ function decode(value) {
5
+ if (!value || typeof value !== 'object')
6
+ return value;
7
+ if (Array.isArray(value))
8
+ return value.map(decode);
9
+ const item = value;
10
+ if (Object.keys(item).length === 1 && Array.isArray(item.__permuteSdkBytes)) {
11
+ if (!item.__permuteSdkBytes.every((byte) => typeof byte === 'number' && Number.isInteger(byte) && byte >= 0 && byte <= 255))
12
+ throw new Error('Invalid SDK byte data.');
13
+ return new Uint8Array(item.__permuteSdkBytes);
14
+ }
15
+ return Object.fromEntries(Object.entries(item).map(([key, child]) => [key, decode(child)]));
16
+ }
17
+ /** Executes the SDK itself behind a JSON RPC boundary; it does not implement API operations. */
18
+ export class PermuteSdkBridge {
19
+ client;
20
+ constructor(client) {
21
+ this.client = client;
22
+ }
23
+ supports(method, acceptsOperation) {
24
+ const selected = this.resolve(method);
25
+ if (!selected)
26
+ return false;
27
+ const operations = SdkMethodFactory.operationsFor(selected.fn);
28
+ return !!operations?.length && (!acceptsOperation || operations.every(acceptsOperation));
29
+ }
30
+ async invoke(method, args) {
31
+ const selected = this.resolve(method);
32
+ if (!selected)
33
+ throw new Error(`Unsupported SDK method: ${method}`);
34
+ const value = await Reflect.apply(selected.fn, selected.owner, args.map(decode));
35
+ // Resource helpers contain callbacks. Their public data is the portable result of an RPC.
36
+ const data = value instanceof PermuteDataset || value instanceof PermuteFileDataset ? value.data : value;
37
+ return JSON.parse(JSON.stringify(data ?? null));
38
+ }
39
+ resolve(method) {
40
+ const names = method.split('.');
41
+ if (names.some((name) => !safeName(name)) || names.length > 2)
42
+ return null;
43
+ const client = this.client;
44
+ if (names.length === 1) {
45
+ const name = names[0];
46
+ const fn = client[name];
47
+ return typeof fn === 'function' && SdkMethodFactory.operationsFor(fn)
48
+ ? { owner: this.client, fn: fn }
49
+ : null;
50
+ }
51
+ const namespace = client[names[0]];
52
+ if (!Object.hasOwn(client, names[0]) ||
53
+ !namespace ||
54
+ typeof namespace !== 'object' ||
55
+ !Object.hasOwn(namespace, names[1]))
56
+ return null;
57
+ const fn = namespace[names[1]];
58
+ return typeof fn === 'function' && SdkMethodFactory.operationsFor(fn)
59
+ ? { owner: namespace, fn: fn }
60
+ : null;
61
+ }
62
+ }