@aithos/sdk 0.1.0-alpha.13 → 0.1.0-alpha.16

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.
@@ -205,6 +205,15 @@ export declare class AithosAuth {
205
205
  importMandate(input: ImportMandateInput): Promise<DelegateInfo>;
206
206
  removeMandate(mandateId: string): Promise<void>;
207
207
  signInWithGoogle(opts?: SignInWithGoogleOptions): never;
208
+ /**
209
+ * Public entrypoint — dedupes concurrent calls (React StrictMode).
210
+ * The first call kicks off the actual exchange; subsequent calls
211
+ * before that promise resolves return the SAME promise so they all
212
+ * receive the same `AithosSession | null`. Otherwise StrictMode's
213
+ * second invocation would race against the URL clean done by the
214
+ * first call and resolve to `null`, robbing the AuthCallback page
215
+ * of the session it actually obtained.
216
+ */
208
217
  handleCallback(): Promise<AithosSession | null>;
209
218
  exchange(aithosCode: string): Promise<AithosSession>;
210
219
  /**
package/dist/src/auth.js CHANGED
@@ -47,6 +47,16 @@ export class AithosAuth {
47
47
  #ownerSigners = null;
48
48
  /** Active delegate registry. */
49
49
  #delegates = new DelegateRegistry();
50
+ /**
51
+ * In-flight (or just-resolved) `handleCallback()` result. React
52
+ * StrictMode (dev) double-invokes the mount effect — the URL clean
53
+ * inside the first call makes the second invocation see a clean URL
54
+ * and resolve to `null`, with the session it just consumed locked
55
+ * inside the first promise. Caching the result here lets both
56
+ * invocations resolve to the same value. Cleared on next mount via
57
+ * the wrapper's once-per-instance dedup.
58
+ */
59
+ #handleCallbackPromise = null;
50
60
  constructor(config = {}) {
51
61
  this.authBaseUrl = trimSlash(config.authBaseUrl ?? DEFAULT_AUTH_BASE_URL);
52
62
  this.apiBaseUrl = trimSlash(config.apiBaseUrl ?? DEFAULT_API_BASE_URL);
@@ -466,7 +476,41 @@ export class AithosAuth {
466
476
  this.#win.location.assign(url.toString());
467
477
  throw new AithosSDKError("auth_redirecting", "redirecting to google");
468
478
  }
479
+ /**
480
+ * Public entrypoint — dedupes concurrent calls (React StrictMode).
481
+ * The first call kicks off the actual exchange; subsequent calls
482
+ * before that promise resolves return the SAME promise so they all
483
+ * receive the same `AithosSession | null`. Otherwise StrictMode's
484
+ * second invocation would race against the URL clean done by the
485
+ * first call and resolve to `null`, robbing the AuthCallback page
486
+ * of the session it actually obtained.
487
+ */
469
488
  async handleCallback() {
489
+ if (!this.#win)
490
+ return null;
491
+ if (this.#handleCallbackPromise)
492
+ return this.#handleCallbackPromise;
493
+ const p = this.#doHandleCallback();
494
+ this.#handleCallbackPromise = p;
495
+ // Clear the cache once the promise settles so a subsequent
496
+ // signInWithGoogle round-trip on the same AithosAuth instance can
497
+ // process its own callback. We use `then(cleanup, cleanup)`
498
+ // rather than `finally(...)` because `finally` re-throws — without
499
+ // a downstream `.catch` the resulting promise becomes an
500
+ // unhandledrejection when `p` itself rejects (the caller already
501
+ // surfaces that rejection via the returned `p`). `then(success,
502
+ // error)` converts a rejection into a clean resolution on this
503
+ // side-effect chain so node:test doesn't flag the orphan as a
504
+ // failure.
505
+ const clear = () => {
506
+ if (this.#handleCallbackPromise === p) {
507
+ this.#handleCallbackPromise = null;
508
+ }
509
+ };
510
+ p.then(clear, clear);
511
+ return p;
512
+ }
513
+ async #doHandleCallback() {
470
514
  if (!this.#win)
471
515
  return null;
472
516
  const here = new URL(this.#win.location.href);
@@ -5,8 +5,18 @@ export interface ComputeMessage {
5
5
  readonly content: string;
6
6
  }
7
7
  export interface InvokeBedrockArgs {
8
- /** Mandate ID granting this app the right to spend the user's wallet. */
9
- readonly mandateId: string;
8
+ /**
9
+ * Mandate ID under which this call should be attributed.
10
+ *
11
+ * - **Owner sessions**: optional. The SDK uses the owner's own DID
12
+ * as a sentinel "self" mandate id — the proxy skips all
13
+ * mandate-related checks (scope, allowed_models, caps) when the
14
+ * envelope is owner-signed, so the value is informational only.
15
+ * - **Delegate sessions**: required. Must reference the imported
16
+ * mandate bundle the SDK signs with (the proxy enforces
17
+ * `compute.invoke` scope and any `allowed_models` filter).
18
+ */
19
+ readonly mandateId?: string;
10
20
  /**
11
21
  * Model id. Today the proxy accepts the canonical Aithos identifiers:
12
22
  * `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-7`.
@@ -44,6 +54,70 @@ export interface InvokeBedrockResult {
44
54
  /** Audit log id for traceability. */
45
55
  readonly auditId: string;
46
56
  }
57
+ /**
58
+ * Stable cross-provider image model ids supported by the Aithos compute
59
+ * proxy. New models can be added on the server side without an SDK
60
+ * release — but tagging the namespaced literal here gives consumers
61
+ * autocomplete + type-checking.
62
+ *
63
+ * The `image:` prefix is part of the wire contract: the server uses it
64
+ * to disambiguate text and image dispatch when a mandate's
65
+ * `allowed_models` mixes both.
66
+ */
67
+ export type ImageModelId = "image:flux-schnell" | "image:flux-dev" | "image:flux-pro-1.1" | "image:flux-pro-1.1-ultra";
68
+ /** Aspect ratios accepted by `invokeImage`. */
69
+ export type ImageAspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "21:9";
70
+ export interface InvokeImageArgs {
71
+ /**
72
+ * Mandate ID under which this call should be attributed.
73
+ *
74
+ * - **Owner sessions**: optional. The SDK uses the owner's own DID
75
+ * as a sentinel "self" mandate id — the proxy skips all
76
+ * mandate-related checks when the envelope is owner-signed.
77
+ * - **Delegate sessions**: required. Must reference the imported
78
+ * mandate bundle the SDK signs with.
79
+ */
80
+ readonly mandateId?: string;
81
+ /**
82
+ * Image model id. Defaults to `"image:flux-pro-1.1"` on the SDK side
83
+ * if omitted — the server allowlist is the source of truth for which
84
+ * ones actually work.
85
+ */
86
+ readonly model?: ImageModelId;
87
+ /** What to draw. */
88
+ readonly prompt: string;
89
+ /** Optional comma-separated list of things to avoid (e.g. "blurry, watermark"). */
90
+ readonly negativePrompt?: string;
91
+ /** Default 1:1. */
92
+ readonly aspectRatio?: ImageAspectRatio;
93
+ /** Deterministic seed (re-rolls). Omit for random. */
94
+ readonly seed?: number;
95
+ /** Number of images to generate. Default 1, max 4. */
96
+ readonly numberOfImages?: number;
97
+ /** Idempotency key for retries (generated if omitted). */
98
+ readonly idempotencyKey?: string;
99
+ /** Abort signal to cancel the request (network + provider call). */
100
+ readonly signal?: AbortSignal;
101
+ }
102
+ export interface InvokeImageImage {
103
+ /** Base64-encoded image bytes (raw, no data: URI prefix). */
104
+ readonly base64: string;
105
+ /** "image/png" | "image/jpeg". */
106
+ readonly contentType: string;
107
+ readonly width: number;
108
+ readonly height: number;
109
+ }
110
+ export interface InvokeImageResult {
111
+ readonly images: readonly InvokeImageImage[];
112
+ /** Seed actually used by the provider (echoed back even when caller didn't set one). */
113
+ readonly seed: number;
114
+ /** Microcredits debited from the wallet (exact — no reconcile path for images). */
115
+ readonly creditsCharged: number;
116
+ /** Wallet balance after debit. */
117
+ readonly walletBalance: number;
118
+ /** Audit log id for traceability. */
119
+ readonly auditId: string;
120
+ }
47
121
  export interface ComputeNamespaceDeps {
48
122
  readonly auth: AithosAuth;
49
123
  readonly appDid: string;
@@ -88,5 +162,25 @@ export declare class ComputeNamespace {
88
162
  * `mandate_revoked`, `insufficient_credits`, …).
89
163
  */
90
164
  invokeBedrock(args: InvokeBedrockArgs): Promise<InvokeBedrockResult>;
165
+ /**
166
+ * Generate one or more images through the Aithos compute proxy
167
+ * (currently powered by fal.ai FLUX models). Spec mirror of
168
+ * {@link invokeBedrock}: same envelope, same wallet path, same
169
+ * mandate-scope gate (`compute.invoke` + `allowed_models`). The
170
+ * separation at the JSON-RPC method level (`aithos.compute_invoke_image`
171
+ * vs `aithos.compute_invoke`) commits each envelope to a specific
172
+ * modality so a stolen text-invoke envelope cannot be replayed
173
+ * against the image endpoint.
174
+ *
175
+ * Default model: `"image:flux-pro-1.1"`. Default aspect ratio: 1:1.
176
+ * Default count: 1 image.
177
+ *
178
+ * Pricing is per image and deterministic (no token-based reconcile):
179
+ * - flux-schnell: 3 000 mc + fee per image
180
+ * - flux-dev: 25 000 mc + fee per image
181
+ * - flux-pro-1.1: 40 000 mc + fee per image
182
+ * - flux-pro-1.1-ultra: 60 000 mc + fee per image
183
+ */
184
+ invokeImage(args: InvokeImageArgs): Promise<InvokeImageResult>;
91
185
  }
92
186
  //# sourceMappingURL=compute.d.ts.map
@@ -60,46 +60,13 @@ export class ComputeNamespace {
60
60
  * `mandate_revoked`, `insufficient_credits`, …).
61
61
  */
62
62
  async invokeBedrock(args) {
63
- const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
64
- const owner = auth._getOwnerSigners();
65
- const ownerLoaded = owner !== null && !owner.destroyed;
66
- let choice;
67
- if (ownerLoaded) {
68
- const publicKp = ownerKeyPair(owner, "public");
69
- choice = {
70
- kind: "owner",
71
- iss: owner.did,
72
- verificationMethod: `${owner.did}#public`,
73
- signer: publicKp,
74
- mandate: undefined,
75
- };
76
- }
77
- else {
78
- // Delegate-only path. Find a session whose mandate id matches.
79
- const actor = auth._getDelegateActor(args.mandateId);
80
- if (!actor || actor.destroyed) {
81
- throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${args.mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
82
- }
83
- const kp = delegateKeyPair(actor);
84
- choice = {
85
- kind: "delegate",
86
- iss: actor.subjectDid,
87
- verificationMethod: actor.granteePubkeyMultibase,
88
- signer: kp,
89
- // The DelegateActor stores the SignedMandate as a structurally
90
- // opaque object so the SDK doesn't have to import the
91
- // protocol-client type at the storage boundary. Round-trip
92
- // through `unknown` for the TS cast — at runtime the bytes are
93
- // the canonical SignedMandate the bundle parser already
94
- // validated.
95
- mandate: actor.mandate,
96
- };
97
- }
63
+ const { endpoints, fetch: fetchImpl } = this.#deps;
64
+ const choice = this.#resolveSigner(args.mandateId);
98
65
  const url = computeInvokeUrl(endpoints);
99
66
  const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
100
67
  const params = {
101
- app_did: appDid,
102
- mandate_id: args.mandateId,
68
+ app_did: this.#deps.appDid,
69
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
103
70
  model: args.model,
104
71
  messages: args.messages,
105
72
  idempotency_key: idempotencyKey,
@@ -110,10 +77,149 @@ export class ComputeNamespace {
110
77
  params.max_tokens = args.maxTokens;
111
78
  if (args.temperature !== undefined)
112
79
  params.temperature = args.temperature;
80
+ return await this.#signAndPost({
81
+ url,
82
+ method: "aithos.compute_invoke",
83
+ params,
84
+ choice,
85
+ fetchImpl,
86
+ signal: args.signal,
87
+ });
88
+ }
89
+ /**
90
+ * Generate one or more images through the Aithos compute proxy
91
+ * (currently powered by fal.ai FLUX models). Spec mirror of
92
+ * {@link invokeBedrock}: same envelope, same wallet path, same
93
+ * mandate-scope gate (`compute.invoke` + `allowed_models`). The
94
+ * separation at the JSON-RPC method level (`aithos.compute_invoke_image`
95
+ * vs `aithos.compute_invoke`) commits each envelope to a specific
96
+ * modality so a stolen text-invoke envelope cannot be replayed
97
+ * against the image endpoint.
98
+ *
99
+ * Default model: `"image:flux-pro-1.1"`. Default aspect ratio: 1:1.
100
+ * Default count: 1 image.
101
+ *
102
+ * Pricing is per image and deterministic (no token-based reconcile):
103
+ * - flux-schnell: 3 000 mc + fee per image
104
+ * - flux-dev: 25 000 mc + fee per image
105
+ * - flux-pro-1.1: 40 000 mc + fee per image
106
+ * - flux-pro-1.1-ultra: 60 000 mc + fee per image
107
+ */
108
+ async invokeImage(args) {
109
+ const { endpoints, fetch: fetchImpl } = this.#deps;
110
+ const choice = this.#resolveSigner(args.mandateId);
111
+ const url = computeInvokeUrl(endpoints);
112
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
113
+ const model = args.model ?? "image:flux-pro-1.1";
114
+ const params = {
115
+ app_did: this.#deps.appDid,
116
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
117
+ model,
118
+ prompt: args.prompt,
119
+ idempotency_key: idempotencyKey,
120
+ };
121
+ if (args.negativePrompt !== undefined)
122
+ params.negative_prompt = args.negativePrompt;
123
+ if (args.aspectRatio !== undefined)
124
+ params.aspect_ratio = args.aspectRatio;
125
+ if (args.seed !== undefined)
126
+ params.seed = args.seed;
127
+ if (args.numberOfImages !== undefined)
128
+ params.number_of_images = args.numberOfImages;
129
+ return await this.#signAndPost({
130
+ url,
131
+ method: "aithos.compute_invoke_image",
132
+ params,
133
+ choice,
134
+ fetchImpl,
135
+ signal: args.signal,
136
+ });
137
+ }
138
+ /**
139
+ * Resolve the active signer (owner takes precedence over delegate).
140
+ *
141
+ * - When an owner is signed in: returns the owner-signer regardless
142
+ * of `mandateId` (the proxy ignores params.mandate_id on
143
+ * owner-signed envelopes, so owners can omit it).
144
+ * - When delegate-only: requires `mandateId` to find the matching
145
+ * imported bundle. Throws `sdk_no_delegate_for_mandate` if absent
146
+ * or no match.
147
+ */
148
+ #resolveSigner(mandateId) {
149
+ const { auth } = this.#deps;
150
+ const owner = auth._getOwnerSigners();
151
+ const ownerLoaded = owner !== null && !owner.destroyed;
152
+ if (ownerLoaded) {
153
+ const publicKp = ownerKeyPair(owner, "public");
154
+ return {
155
+ kind: "owner",
156
+ iss: owner.did,
157
+ verificationMethod: `${owner.did}#public`,
158
+ signer: publicKp,
159
+ mandate: undefined,
160
+ };
161
+ }
162
+ if (mandateId === undefined || mandateId.length === 0) {
163
+ throw new AithosSDKError("sdk_no_signer", "no owner signed in and no mandateId provided — pass a mandateId for a delegate session, or sign in as an owner first.");
164
+ }
165
+ const actor = auth._getDelegateActor(mandateId);
166
+ if (!actor || actor.destroyed) {
167
+ throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
168
+ }
169
+ const kp = delegateKeyPair(actor);
170
+ return {
171
+ kind: "delegate",
172
+ iss: actor.subjectDid,
173
+ verificationMethod: actor.granteePubkeyMultibase,
174
+ signer: kp,
175
+ // The DelegateActor stores the SignedMandate as a structurally
176
+ // opaque object so the SDK doesn't have to import the
177
+ // protocol-client type at the storage boundary. Round-trip
178
+ // through `unknown` for the TS cast — at runtime the bytes are
179
+ // the canonical SignedMandate the bundle parser already
180
+ // validated.
181
+ mandate: actor.mandate,
182
+ };
183
+ }
184
+ /**
185
+ * Resolve the `mandate_id` value the SDK writes into the JSON-RPC
186
+ * params for the wire. The proxy requires this field to be a
187
+ * non-empty string but only consults it for the delegate path —
188
+ * for owner-signed envelopes it's audit-log metadata, with no
189
+ * security implication.
190
+ *
191
+ * Strategy:
192
+ * - Caller-provided id always wins (owner who wants to attribute
193
+ * to a specific minted mandate can still pass it).
194
+ * - Delegate path: the choice carries the real mandate.id — use it
195
+ * so a delegate cannot accidentally lie about which mandate
196
+ * it claims to spend under.
197
+ * - Owner path with no explicit id: use the owner DID followed
198
+ * by `#self` — a stable sentinel that's unambiguously not a
199
+ * real mandate id (mandate ids are ULIDs).
200
+ */
201
+ #resolveMandateIdForWire(explicit, choice) {
202
+ if (explicit && explicit.length > 0)
203
+ return explicit;
204
+ if (choice.kind === "delegate")
205
+ return choice.mandate.id;
206
+ // Owner-direct sentinel. The proxy never inspects this value
207
+ // beyond non-empty-string validation when no mandate is attached.
208
+ return `${choice.iss}#self`;
209
+ }
210
+ /**
211
+ * Sign the params with the resolved signer and POST a JSON-RPC
212
+ * request. Shared between `invokeBedrock` and `invokeImage` — the
213
+ * only difference between those two is the method name and the
214
+ * params shape; the envelope construction + transport + error
215
+ * mapping is identical.
216
+ */
217
+ async #signAndPost(opts) {
218
+ const { url, method, params, choice, fetchImpl, signal } = opts;
113
219
  const envelope = buildSignedEnvelope({
114
220
  iss: choice.iss,
115
221
  aud: url,
116
- method: "aithos.compute_invoke",
222
+ method,
117
223
  verificationMethod: choice.verificationMethod,
118
224
  params,
119
225
  signer: choice.signer,
@@ -126,11 +232,11 @@ export class ComputeNamespace {
126
232
  headers: { "content-type": "application/json" },
127
233
  body: JSON.stringify({
128
234
  jsonrpc: "2.0",
129
- id: "aithos.compute_invoke",
130
- method: "aithos.compute_invoke",
235
+ id: method,
236
+ method,
131
237
  params: { ...params, _envelope: envelope },
132
238
  }),
133
- ...(args.signal ? { signal: args.signal } : {}),
239
+ ...(signal ? { signal } : {}),
134
240
  });
135
241
  }
136
242
  catch (e) {
@@ -1,11 +1,11 @@
1
- export declare const VERSION = "0.1.0-alpha.5";
1
+ export declare const VERSION = "0.1.0-alpha.16";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, InvokeBedrockArgs, InvokeBedrockResult, StopReason, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, StopReason, } from "./compute.js";
9
9
  export { ComputeNamespace } from "./compute.js";
10
10
  export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
11
11
  export { WalletNamespace } from "./wallet.js";
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
18
18
  // are exported from here. Endpoint config (`AithosSdkEndpoints`,
19
19
  // `DEFAULT_SDK_ENDPOINTS`) likewise.
20
- export const VERSION = "0.1.0-alpha.5";
20
+ export const VERSION = "0.1.0-alpha.16";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
23
  // Re-export protocol-client's JSON-RPC error type so consumers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.16",
4
4
  "description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
5
5
  "keywords": [
6
6
  "aithos",
@@ -39,15 +39,6 @@
39
39
  "README.md",
40
40
  "LICENSE"
41
41
  ],
42
- "scripts": {
43
- "build": "tsc",
44
- "build:test": "tsc -p tsconfig.test.json",
45
- "check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
46
- "test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
47
- "test:watch": "cd dist && node --test --watch",
48
- "clean": "rm -rf dist",
49
- "prepublishOnly": "npm run clean && npm run build && npm test"
50
- },
51
42
  "engines": {
52
43
  "node": ">=20"
53
44
  },
@@ -63,5 +54,13 @@
63
54
  "publishConfig": {
64
55
  "access": "public",
65
56
  "tag": "alpha"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc",
60
+ "build:test": "tsc -p tsconfig.test.json",
61
+ "check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
62
+ "test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
63
+ "test:watch": "cd dist && node --test --watch",
64
+ "clean": "rm -rf dist"
66
65
  }
67
- }
66
+ }