@scenario-labs/sdk 2.2.0 → 2.3.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/index.d.mts +1 -0
  3. package/index.d.mts.map +1 -1
  4. package/index.d.ts +1 -0
  5. package/index.d.ts.map +1 -1
  6. package/index.js +3 -1
  7. package/index.js.map +1 -1
  8. package/index.mjs +1 -0
  9. package/index.mjs.map +1 -1
  10. package/lib/auth-refresh.d.mts +52 -0
  11. package/lib/auth-refresh.d.mts.map +1 -0
  12. package/lib/auth-refresh.d.ts +52 -0
  13. package/lib/auth-refresh.d.ts.map +1 -0
  14. package/lib/auth-refresh.js +81 -0
  15. package/lib/auth-refresh.js.map +1 -0
  16. package/lib/auth-refresh.mjs +78 -0
  17. package/lib/auth-refresh.mjs.map +1 -0
  18. package/lib/generate.js +7 -7
  19. package/lib/generate.js.map +1 -1
  20. package/lib/generate.mjs +7 -7
  21. package/lib/generate.mjs.map +1 -1
  22. package/lib/job.d.mts +15 -3
  23. package/lib/job.d.mts.map +1 -1
  24. package/lib/job.d.ts +15 -3
  25. package/lib/job.d.ts.map +1 -1
  26. package/lib/job.js +22 -8
  27. package/lib/job.js.map +1 -1
  28. package/lib/job.mjs +22 -8
  29. package/lib/job.mjs.map +1 -1
  30. package/lib/models.d.mts +8 -2
  31. package/lib/models.d.mts.map +1 -1
  32. package/lib/models.d.ts +8 -2
  33. package/lib/models.d.ts.map +1 -1
  34. package/lib/models.js +13 -5
  35. package/lib/models.js.map +1 -1
  36. package/lib/models.mjs +13 -5
  37. package/lib/models.mjs.map +1 -1
  38. package/lib/scenario.d.mts +28 -13
  39. package/lib/scenario.d.mts.map +1 -1
  40. package/lib/scenario.d.ts +28 -13
  41. package/lib/scenario.d.ts.map +1 -1
  42. package/lib/scenario.js +31 -12
  43. package/lib/scenario.js.map +1 -1
  44. package/lib/scenario.mjs +31 -12
  45. package/lib/scenario.mjs.map +1 -1
  46. package/lib/scope.d.mts +41 -0
  47. package/lib/scope.d.mts.map +1 -0
  48. package/lib/scope.d.ts +41 -0
  49. package/lib/scope.d.ts.map +1 -0
  50. package/lib/scope.js +55 -0
  51. package/lib/scope.js.map +1 -0
  52. package/lib/scope.mjs +49 -0
  53. package/lib/scope.mjs.map +1 -0
  54. package/lib/upload.d.mts +15 -3
  55. package/lib/upload.d.mts.map +1 -1
  56. package/lib/upload.d.ts +15 -3
  57. package/lib/upload.d.ts.map +1 -1
  58. package/lib/upload.js +29 -12
  59. package/lib/upload.js.map +1 -1
  60. package/lib/upload.mjs +29 -12
  61. package/lib/upload.mjs.map +1 -1
  62. package/lib/workflow.d.mts +8 -2
  63. package/lib/workflow.d.mts.map +1 -1
  64. package/lib/workflow.d.ts +8 -2
  65. package/lib/workflow.d.ts.map +1 -1
  66. package/lib/workflow.js +14 -6
  67. package/lib/workflow.js.map +1 -1
  68. package/lib/workflow.mjs +14 -6
  69. package/lib/workflow.mjs.map +1 -1
  70. package/lib/workflow_converter.d.mts +2 -5
  71. package/lib/workflow_converter.d.mts.map +1 -1
  72. package/lib/workflow_converter.d.ts +2 -5
  73. package/lib/workflow_converter.d.ts.map +1 -1
  74. package/lib/workflow_converter.js +38 -3
  75. package/lib/workflow_converter.js.map +1 -1
  76. package/lib/workflow_converter.mjs +38 -3
  77. package/lib/workflow_converter.mjs.map +1 -1
  78. package/lib/workflow_import_validator.d.mts +1 -1
  79. package/lib/workflow_import_validator.d.mts.map +1 -1
  80. package/lib/workflow_import_validator.d.ts +1 -1
  81. package/lib/workflow_import_validator.d.ts.map +1 -1
  82. package/lib/workflow_import_validator.js +1 -0
  83. package/lib/workflow_import_validator.js.map +1 -1
  84. package/lib/workflow_import_validator.mjs +1 -0
  85. package/lib/workflow_import_validator.mjs.map +1 -1
  86. package/package.json +1 -1
  87. package/resources/assets/assets.d.mts +1 -1
  88. package/resources/assets/assets.d.mts.map +1 -1
  89. package/resources/assets/assets.d.ts +1 -1
  90. package/resources/assets/assets.d.ts.map +1 -1
  91. package/resources/generate.d.mts +8 -8
  92. package/resources/generate.d.mts.map +1 -1
  93. package/resources/generate.d.ts +8 -8
  94. package/resources/generate.d.ts.map +1 -1
  95. package/resources/jobs.d.mts +3 -3
  96. package/resources/jobs.d.mts.map +1 -1
  97. package/resources/jobs.d.ts +3 -3
  98. package/resources/jobs.d.ts.map +1 -1
  99. package/resources/models/examples.d.mts +2 -2
  100. package/resources/models/examples.d.mts.map +1 -1
  101. package/resources/models/examples.d.ts +2 -2
  102. package/resources/models/examples.d.ts.map +1 -1
  103. package/resources/models/models.d.mts +126 -7
  104. package/resources/models/models.d.mts.map +1 -1
  105. package/resources/models/models.d.ts +126 -7
  106. package/resources/models/models.d.ts.map +1 -1
  107. package/resources/models/models.js.map +1 -1
  108. package/resources/models/models.mjs.map +1 -1
  109. package/resources/models/train.d.mts +37 -3
  110. package/resources/models/train.d.mts.map +1 -1
  111. package/resources/models/train.d.ts +37 -3
  112. package/resources/models/train.d.ts.map +1 -1
  113. package/resources/usages.d.mts +1 -1
  114. package/resources/usages.d.mts.map +1 -1
  115. package/resources/usages.d.ts +1 -1
  116. package/resources/usages.d.ts.map +1 -1
  117. package/resources/workflows.d.mts +22 -5
  118. package/resources/workflows.d.mts.map +1 -1
  119. package/resources/workflows.d.ts +22 -5
  120. package/resources/workflows.d.ts.map +1 -1
  121. package/resources/workflows.js +4 -1
  122. package/resources/workflows.js.map +1 -1
  123. package/resources/workflows.mjs +4 -1
  124. package/resources/workflows.mjs.map +1 -1
  125. package/src/index.ts +1 -0
  126. package/src/lib/auth-refresh.ts +98 -0
  127. package/src/lib/generate.ts +7 -7
  128. package/src/lib/job.ts +29 -7
  129. package/src/lib/models.ts +14 -4
  130. package/src/lib/scenario.ts +51 -13
  131. package/src/lib/scope.ts +68 -0
  132. package/src/lib/upload.ts +36 -11
  133. package/src/lib/workflow.ts +15 -5
  134. package/src/lib/workflow_converter.ts +47 -6
  135. package/src/lib/workflow_import_validator.ts +3 -1
  136. package/src/resources/assets/assets.ts +1 -1
  137. package/src/resources/generate.ts +8 -8
  138. package/src/resources/jobs.ts +3 -3
  139. package/src/resources/models/examples.ts +2 -2
  140. package/src/resources/models/models.ts +189 -7
  141. package/src/resources/models/train.ts +55 -3
  142. package/src/resources/usages.ts +1 -1
  143. package/src/resources/workflows.ts +24 -5
  144. package/src/version.ts +1 -1
  145. package/version.d.mts +1 -1
  146. package/version.d.ts +1 -1
  147. package/version.js +1 -1
  148. package/version.mjs +1 -1
package/src/lib/job.ts CHANGED
@@ -7,12 +7,19 @@ import {
7
7
  type JobTriggerActionParams,
8
8
  type JobTriggerActionResponse,
9
9
  } from '../resources/jobs';
10
+ import { type Scope, clientProjectId, effectiveScope, withScope } from './scope';
10
11
 
11
12
  export interface WaitOptions {
12
13
  /** Polling interval in milliseconds. Default: 3000 */
13
14
  intervalMs?: number;
14
15
  /** Maximum wait time in milliseconds. Default: 120000 */
15
16
  timeoutMs?: number;
17
+ /**
18
+ * Override the project scope used for the polling requests. Defaults to
19
+ * the scope captured on the job (per-call override on the originating
20
+ * call, else the client's default project).
21
+ */
22
+ projectId?: string;
16
23
  }
17
24
 
18
25
  type JobStatus = JobRetrieveResponse.Job['status'];
@@ -22,6 +29,8 @@ const TERMINAL_STATUSES: Set<JobStatus> = new Set(['success', 'failure', 'cancel
22
29
  /** Job methods added on top of the original job fields. */
23
30
  export class JobMethods {
24
31
  /** @internal */ declare readonly _client: Scenario;
32
+ /** @internal scope captured from the originating call — replayed on every `.wait()` poll. */
33
+ declare readonly _scope?: Scope;
25
34
 
26
35
  /**
27
36
  * Poll until the job reaches a terminal status (success, failure, or canceled).
@@ -36,13 +45,14 @@ export class JobMethods {
36
45
  async wait(this: Job, options?: WaitOptions): Promise<Job> {
37
46
  const intervalMs = options?.intervalMs ?? 3_000;
38
47
  const timeoutMs = options?.timeoutMs ?? 120_000;
48
+ const scope = waitOverrideScope(options) ?? this._scope;
39
49
  let elapsed = 0;
40
50
 
41
51
  while (elapsed < timeoutMs) {
42
- const response = await this._client.jobs.retrieve(this.jobId);
52
+ const response = await this._client.jobs.retrieve(this.jobId, withScope(scope));
43
53
 
44
54
  if (TERMINAL_STATUSES.has(response.job.status)) {
45
- return Job.from(this._client, response.job);
55
+ return Job.from(this._client, response.job, scope);
46
56
  }
47
57
 
48
58
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
@@ -52,10 +62,11 @@ export class JobMethods {
52
62
  throw new Error(`Job ${this.jobId} did not complete within ${timeoutMs / 1_000}s`);
53
63
  }
54
64
 
55
- /** @internal Create a Job from raw job data. */
56
- static from(client: Scenario, data: JobRetrieveResponse['job']): Job {
65
+ /** @internal Create a Job from raw job data, optionally remembering the originating scope. */
66
+ static from(client: Scenario, data: JobRetrieveResponse['job'], scope?: Scope): Job {
57
67
  const job = Object.assign(Object.create(JobMethods.prototype), data) as Job;
58
68
  Object.defineProperty(job, '_client', { value: client, enumerable: false });
69
+ if (scope) Object.defineProperty(job, '_scope', { value: scope, enumerable: false });
59
70
  return job;
60
71
  }
61
72
  }
@@ -72,14 +83,19 @@ export type WithJob<T extends { job: unknown }> = T & { job: JobMethods };
72
83
 
73
84
  /**
74
85
  * @internal Wrap `_thenUnwrap` to replace `response.job` with an enhanced Job.
86
+ * Captures the effective scope (per-call override or client default) so
87
+ * `.wait()` can replay it on every poll — keeping follow-up calls in sync
88
+ * with the project the job was originally created in.
75
89
  */
76
90
  export function enhanceJob<T extends { job: { jobId: string; status: string } }>(
77
91
  client: Scenario,
78
92
  promise: APIPromise<T>,
93
+ options?: RequestOptions,
79
94
  ): APIPromise<WithJob<T>> {
95
+ const scope = effectiveScope(options, clientProjectId(client));
80
96
  return promise._thenUnwrap((data) => ({
81
97
  ...data,
82
- job: Job.from(client, data.job as JobRetrieveResponse['job']),
98
+ job: Job.from(client, data.job as JobRetrieveResponse['job'], scope),
83
99
  }));
84
100
  }
85
101
 
@@ -92,7 +108,7 @@ export function enhanceJob<T extends { job: { jobId: string; status: string } }>
92
108
  */
93
109
  export class EnhancedJobs extends Jobs {
94
110
  override retrieve(jobID: string, options?: RequestOptions): APIPromise<WithJob<JobRetrieveResponse>> {
95
- return enhanceJob(this._client, super.retrieve(jobID, options));
111
+ return enhanceJob(this._client, super.retrieve(jobID, options), options);
96
112
  }
97
113
 
98
114
  override triggerAction(
@@ -100,6 +116,12 @@ export class EnhancedJobs extends Jobs {
100
116
  params: JobTriggerActionParams,
101
117
  options?: RequestOptions,
102
118
  ): APIPromise<WithJob<JobTriggerActionResponse>> {
103
- return enhanceJob(this._client, super.triggerAction(jobID, params, options));
119
+ return enhanceJob(this._client, super.triggerAction(jobID, params, options), options);
104
120
  }
105
121
  }
122
+
123
+ /** Resolve the scope override carried by a `WaitOptions`, if any. */
124
+ function waitOverrideScope(options?: WaitOptions): Scope | undefined {
125
+ if (!options?.projectId) return undefined;
126
+ return { projectId: options.projectId };
127
+ }
package/src/lib/models.ts CHANGED
@@ -4,6 +4,7 @@ import type { GenerateRunModelParams, GenerateRunModelResponse } from '../resour
4
4
  import { Models, type ModelRetrieveParams, type ModelRetrieveResponse } from '../resources/models/models';
5
5
  import { Train, type TrainTriggerParams, type TrainTriggerResponse } from '../resources/models/train';
6
6
  import { type WithJob, enhanceJob } from './job';
7
+ import { type Scope, effectiveScope, withScope } from './scope';
7
8
  // Type-only import to break the circular `scenario.ts` ↔ `models.ts` graph at runtime.
8
9
  import type { Scenario } from './scenario';
9
10
 
@@ -20,28 +21,35 @@ import type { Scenario } from './scenario';
20
21
  */
21
22
  export class ModelEntity {
22
23
  /** @internal */ declare readonly _client: Scenario;
24
+ /** @internal scope captured from the retrieve call — replayed when `.run()` is invoked without an explicit query override. */
25
+ declare readonly _scope?: Scope;
23
26
 
24
27
  /**
25
28
  * Generate with this model. Shortcut for
26
29
  * `client.generate.runModel(this.id, params)`.
27
30
  *
28
31
  * Returns an enhanced response where `response.job` has `.wait()`.
32
+ * If the model was fetched with a `projectId` scope (per-call override or
33
+ * client default), that scope is reused unless overridden by
34
+ * `options.query`.
29
35
  */
30
36
  run(
31
37
  this: ModelEntity & ModelRetrieveResponse.Model,
32
38
  params: GenerateRunModelParams,
33
39
  options?: RequestOptions,
34
40
  ): APIPromise<WithJob<GenerateRunModelResponse>> {
35
- return this._client.generate.runModel(this.id, params, options);
41
+ return this._client.generate.runModel(this.id, params, withScope(this._scope, options));
36
42
  }
37
43
 
38
- /** @internal Create a ModelEntity wrapping raw model data. */
44
+ /** @internal Create a ModelEntity wrapping raw model data, optionally remembering the originating scope. */
39
45
  static from(
40
46
  client: Scenario,
41
47
  data: ModelRetrieveResponse.Model,
48
+ scope?: Scope,
42
49
  ): ModelRetrieveResponse.Model & ModelEntity {
43
50
  const entity = Object.assign(Object.create(ModelEntity.prototype), data);
44
51
  Object.defineProperty(entity, '_client', { value: client, enumerable: false });
52
+ if (scope) Object.defineProperty(entity, '_scope', { value: scope, enumerable: false });
45
53
  return entity;
46
54
  }
47
55
  }
@@ -61,7 +69,7 @@ export class EnhancedTrain extends Train {
61
69
  params: TrainTriggerParams,
62
70
  options?: RequestOptions,
63
71
  ): APIPromise<WithJob<TrainTriggerResponse>> {
64
- return enhanceJob(this._client, super.trigger(modelID, params, options));
72
+ return enhanceJob(this._client, super.trigger(modelID, params, options), options);
65
73
  }
66
74
  }
67
75
 
@@ -79,9 +87,11 @@ export class EnhancedModels extends Models {
79
87
  query: ModelRetrieveParams | null | undefined = {},
80
88
  options?: RequestOptions,
81
89
  ): APIPromise<EnhancedModelRetrieveResponse> {
90
+ const client = this._client as Scenario;
91
+ const scope = effectiveScope(options, client.projectId);
82
92
  return super.retrieve(modelID, query, options)._thenUnwrap((data) => ({
83
93
  ...data,
84
- model: ModelEntity.from(this._client as Scenario, data.model),
94
+ model: ModelEntity.from(client, data.model, scope),
85
95
  }));
86
96
  }
87
97
  }
@@ -1,4 +1,4 @@
1
- import { Scenario as BaseScenario } from '../client';
1
+ import { Scenario as BaseScenario, type ClientOptions as BaseClientOptions } from '../client';
2
2
  import { EnhancedAssets } from './asset';
3
3
  import { EnhancedGenerate } from './generate';
4
4
  import { EnhancedJobs } from './job';
@@ -6,6 +6,23 @@ import { EnhancedModels } from './models';
6
6
  import { EnhancedUploads } from './upload';
7
7
  import { EnhancedWorkflows } from './workflow';
8
8
 
9
+ export interface ClientOptions extends BaseClientOptions {
10
+ /**
11
+ * Default project to scope every request to (sent as `?projectId=...`).
12
+ *
13
+ * Useful with `bearerAuth` when the user belongs to multiple projects —
14
+ * the server derives the team from the project, so `teamId` doesn't need
15
+ * to be configured at construction time. With `apiKey` auth, the project
16
+ * is already implied by the key and this option is unnecessary.
17
+ *
18
+ * Per-call `options.query.projectId` overrides this default for a single
19
+ * request. SDK helpers (`.wait()`, `model.run()`, ...) reuse the scope
20
+ * effective on the originating call, so an action created in project A
21
+ * keeps polling in project A even if the client default changes later.
22
+ */
23
+ projectId?: string | null | undefined;
24
+ }
25
+
9
26
  /**
10
27
  * Enhanced Scenario client.
11
28
  *
@@ -17,23 +34,19 @@ import { EnhancedWorkflows } from './workflow';
17
34
  * ```ts
18
35
  * import Scenario from '@scenario-labs/sdk';
19
36
  *
20
- * const client = new Scenario({ apiKey: '...' });
37
+ * // With API key — project is implied by the key.
38
+ * const client = new Scenario({ apiKey: '...', apiSecret: '...' });
39
+ *
40
+ * // With bearer JWT — pin a default project for all requests.
41
+ * const client = new Scenario({ bearerAuth: jwt, projectId: 'proj_abc' });
21
42
  *
22
- * // All generated methods work as-is
23
- * const list = await client.assets.list({});
43
+ * // Per-call override (one request only):
44
+ * await client.uploads.uploadFile(params, { query: { projectId: 'proj_other' } });
24
45
  *
25
- * // Enhanced: response.job.wait()
46
+ * // Enhanced: response.job.wait() — auto-reuses the scope of the call that produced the job
26
47
  * const run = await client.workflows.run(workflowId, { body });
27
48
  * const completed = await run.job.wait();
28
49
  *
29
- * // Enhanced: response.workflow.findNode()
30
- * const wf = await client.workflows.retrieve(workflowId);
31
- * wf.workflow.findNode('final_image');
32
- *
33
- * // Enhanced: generate job.wait()
34
- * const gen = await client.generate.runModel(modelId, { body });
35
- * const done = await gen.job.wait();
36
- *
37
50
  * // Enhanced: upload.wait()
38
51
  * const res = await client.uploads.retrieve(uploadId);
39
52
  * const imported = await res.upload.wait();
@@ -50,4 +63,29 @@ export class Scenario extends BaseScenario {
50
63
  override uploads: EnhancedUploads = new EnhancedUploads(this);
51
64
  override assets: EnhancedAssets = new EnhancedAssets(this);
52
65
  override jobs: EnhancedJobs = new EnhancedJobs(this);
66
+
67
+ /** The default project this client scopes every request to, or `null` if unset. */
68
+ readonly projectId: string | null;
69
+
70
+ constructor(options: ClientOptions = {}) {
71
+ const { projectId, defaultQuery, ...rest } = options;
72
+ // Strip any stale projectId from defaultQuery so the typed field is the
73
+ // single source of truth (matters when `withOptions` reconstructs the client).
74
+ const cleanedDefaultQuery = stripKey(defaultQuery, 'projectId');
75
+ super({
76
+ ...rest,
77
+ // Pass projectId through so it lands in `_options` and survives a
78
+ // `withOptions()` round-trip. The base ClientOptions doesn't declare
79
+ // this field, hence the cast.
80
+ projectId,
81
+ defaultQuery: projectId ? { ...cleanedDefaultQuery, projectId } : cleanedDefaultQuery,
82
+ } as BaseClientOptions);
83
+ this.projectId = projectId ?? null;
84
+ }
85
+ }
86
+
87
+ function stripKey<T extends object>(obj: T | undefined, key: string): Partial<T> | undefined {
88
+ if (!obj || !(key in obj)) return obj;
89
+ const { [key]: _omit, ...rest } = obj as Record<string, unknown>;
90
+ return rest as Partial<T>;
53
91
  }
@@ -0,0 +1,68 @@
1
+ import type { RequestOptions } from '../internal/request-options';
2
+
3
+ /**
4
+ * Per-request project scope.
5
+ *
6
+ * With API-key auth, the server resolves the project from the key. With
7
+ * bearer (JWT) auth a user can belong to multiple projects, so callers must
8
+ * specify which one to act on by passing `projectId` as a query param
9
+ * (the server derives `teamId` from it — no need to send it explicitly).
10
+ *
11
+ * SDK helpers like `Job.wait()` and `Upload.wait()` make follow-up calls
12
+ * internally — they capture the scope effective on the originating call and
13
+ * forward it on each retry, so an action created in project A always polls
14
+ * in project A even if the client default changes between calls.
15
+ */
16
+ export interface Scope {
17
+ projectId?: string;
18
+ }
19
+
20
+ /** Extract `projectId` from a `RequestOptions.query`. Returns `undefined` if absent. */
21
+ export function extractScope(options?: RequestOptions): Scope | undefined {
22
+ const query = options?.query as Record<string, unknown> | undefined;
23
+ if (!query) return undefined;
24
+ const projectId = typeof query['projectId'] === 'string' ? query['projectId'] : undefined;
25
+ if (!projectId) return undefined;
26
+ return { projectId };
27
+ }
28
+
29
+ /**
30
+ * Resolve the effective scope for an outgoing call:
31
+ * 1. If the per-call options carry a `projectId` query — use that (the explicit override wins).
32
+ * 2. Otherwise, fall back to the client's default `projectId`.
33
+ * 3. If neither is set (e.g. API-key auth with implicit scope), return `undefined`.
34
+ *
35
+ * Returns the snapshot we want to *capture on an enhanced entity* so that
36
+ * follow-up helper calls (`.wait()`, `entity.run()`, ...) reuse the same
37
+ * scope and stay in sync with the call that produced them.
38
+ */
39
+ export function effectiveScope(
40
+ options: RequestOptions | undefined,
41
+ fallbackProjectId: string | null | undefined,
42
+ ): Scope | undefined {
43
+ const override = extractScope(options);
44
+ if (override) return override;
45
+ if (fallbackProjectId) return { projectId: fallbackProjectId };
46
+ return undefined;
47
+ }
48
+
49
+ /**
50
+ * Read the client's default `projectId` without repeating the structural
51
+ * cast at every call site. The base generated `Scenario` type doesn't
52
+ * declare `projectId` — only the enhanced one in `./scenario` does — so
53
+ * `lib/` modules that import `Scenario` from `'../client'` use this helper
54
+ * instead of duplicating the same cast inline.
55
+ */
56
+ export function clientProjectId(client: unknown): string | null | undefined {
57
+ return (client as { projectId?: string | null }).projectId;
58
+ }
59
+
60
+ /** Build a `RequestOptions` that adds the scope as a query param, merging into any existing query. */
61
+ export function withScope(scope: Scope | undefined, options?: RequestOptions): RequestOptions | undefined {
62
+ if (!scope?.projectId) return options;
63
+ const existingQuery = (options?.query as Record<string, unknown> | undefined) ?? {};
64
+ return {
65
+ ...options,
66
+ query: { ...existingQuery, projectId: scope.projectId },
67
+ };
68
+ }
package/src/lib/upload.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from '../resources/uploads';
12
12
  import type { AssetRetrieveResponse } from '../resources/assets/assets';
13
13
  import type { ModelRetrieveResponse } from '../resources/models/models';
14
+ import { type Scope, clientProjectId, effectiveScope, withScope } from './scope';
14
15
 
15
16
  type UploadKind = NonNullable<UploadCreateParams['kind']>;
16
17
 
@@ -81,11 +82,19 @@ export interface UploadWaitOptions {
81
82
  intervalMs?: number;
82
83
  /** Maximum wait time in milliseconds. Default: 300000 */
83
84
  timeoutMs?: number;
85
+ /**
86
+ * Override the project scope used for the polling requests. Defaults to
87
+ * the scope captured on the upload (per-call override on the originating
88
+ * call, else the client's default project).
89
+ */
90
+ projectId?: string;
84
91
  }
85
92
 
86
93
  /** Upload methods added on top of the original upload fields. */
87
94
  export class UploadMethods {
88
95
  /** @internal */ declare readonly _client: Scenario;
96
+ /** @internal scope captured from the originating call — replayed on every `.wait()` poll. */
97
+ declare readonly _scope?: Scope;
89
98
 
90
99
  /**
91
100
  * Poll until the upload has been processed into an entity (asset or model),
@@ -102,18 +111,20 @@ export class UploadMethods {
102
111
  * ```
103
112
  */
104
113
  async wait(this: Upload, options?: UploadWaitOptions): Promise<Upload> {
114
+ const scope = waitOverrideScope(options) ?? this._scope;
105
115
  const final = await pollUntilImported(
106
- () => this._client.uploads.retrieve(this.id),
116
+ () => this._client.uploads.retrieve(this.id, withScope(scope)),
107
117
  options?.intervalMs ?? 2_000,
108
118
  options?.timeoutMs ?? 300_000,
109
119
  );
110
- return UploadMethods.from(this._client, final.upload);
120
+ return UploadMethods.from(this._client, final.upload, scope);
111
121
  }
112
122
 
113
- /** @internal Create an Upload from raw upload data. */
114
- static from(client: Scenario, data: UploadRetrieveResponse['upload']): Upload {
123
+ /** @internal Create an Upload from raw upload data, optionally remembering the originating scope. */
124
+ static from(client: Scenario, data: UploadRetrieveResponse['upload'], scope?: Scope): Upload {
115
125
  const upload = Object.assign(Object.create(UploadMethods.prototype), data) as Upload;
116
126
  Object.defineProperty(upload, '_client', { value: client, enumerable: false });
127
+ if (scope) Object.defineProperty(upload, '_scope', { value: scope, enumerable: false });
117
128
  return upload;
118
129
  }
119
130
  }
@@ -130,14 +141,19 @@ export type WithUpload<T extends { upload: unknown }> = T & { upload: UploadMeth
130
141
 
131
142
  /**
132
143
  * @internal Wrap `_thenUnwrap` to replace `response.upload` with an enhanced Upload.
144
+ * Captures the effective scope (per-call override or client default) so
145
+ * `.wait()` can replay it on every poll — keeping follow-up calls in sync
146
+ * with the project the upload was originally created in.
133
147
  */
134
148
  export function enhanceUpload<T extends { upload: { id: string; status: string } }>(
135
149
  client: Scenario,
136
150
  promise: APIPromise<T>,
151
+ options?: RequestOptions,
137
152
  ): APIPromise<WithUpload<T>> {
153
+ const scope = effectiveScope(options, clientProjectId(client));
138
154
  return promise._thenUnwrap((data) => ({
139
155
  ...data,
140
- upload: Upload.from(client, data.upload as UploadRetrieveResponse['upload']),
156
+ upload: Upload.from(client, data.upload as UploadRetrieveResponse['upload'], scope),
141
157
  }));
142
158
  }
143
159
 
@@ -153,14 +169,14 @@ export class EnhancedUploads extends Uploads {
153
169
  body: UploadCreateParams,
154
170
  options?: RequestOptions,
155
171
  ): APIPromise<WithUpload<UploadCreateResponse>> {
156
- return enhanceUpload(this._client, super.create(body, options));
172
+ return enhanceUpload(this._client, super.create(body, options), options);
157
173
  }
158
174
 
159
175
  override retrieve(
160
176
  uploadID: string,
161
177
  options?: RequestOptions,
162
178
  ): APIPromise<WithUpload<UploadRetrieveResponse>> {
163
- return enhanceUpload(this._client, super.retrieve(uploadID, options));
179
+ return enhanceUpload(this._client, super.retrieve(uploadID, options), options);
164
180
  }
165
181
 
166
182
  override triggerAction(
@@ -168,7 +184,7 @@ export class EnhancedUploads extends Uploads {
168
184
  body: UploadTriggerActionParams,
169
185
  options?: RequestOptions,
170
186
  ): APIPromise<WithUpload<UploadTriggerActionResponse>> {
171
- return enhanceUpload(this._client, super.triggerAction(uploadID, body, options));
187
+ return enhanceUpload(this._client, super.triggerAction(uploadID, body, options), options);
172
188
  }
173
189
 
174
190
  /**
@@ -266,16 +282,25 @@ export class EnhancedUploads extends Uploads {
266
282
  throw new Error(`Upload ${uploadId} reached status "${final.upload.status}" but has no entityId`);
267
283
  }
268
284
 
269
- // Fetch and return the resolved entity (narrowed by kind).
285
+ // Fetch and return the resolved entity (narrowed by kind). Forward the
286
+ // original options so any per-call `projectId` query is passed through
287
+ // to the final retrieve (necessary under bearer auth with per-call
288
+ // scope overrides).
270
289
  if (params.kind === 'model') {
271
- const model = await this._client.models.retrieve(entityId);
290
+ const model = await this._client.models.retrieve(entityId, undefined, options);
272
291
  return model as UploadResult<K>;
273
292
  }
274
- const asset = await this._client.assets.retrieve(entityId);
293
+ const asset = await this._client.assets.retrieve(entityId, undefined, options);
275
294
  return asset as UploadResult<K>;
276
295
  }
277
296
  }
278
297
 
298
+ /** Resolve the scope override carried by an `UploadWaitOptions`, if any. */
299
+ function waitOverrideScope(options?: UploadWaitOptions): Scope | undefined {
300
+ if (!options?.projectId) return undefined;
301
+ return { projectId: options.projectId };
302
+ }
303
+
279
304
  // ─────────────────────── helpers ───────────────────────
280
305
 
281
306
  async function normalizeToBytes(input: UploadFileInput): Promise<Uint8Array> {
@@ -9,6 +9,7 @@ import {
9
9
  type WorkflowRunResponse,
10
10
  } from '../resources/workflows';
11
11
  import { type WithJob, enhanceJob } from './job';
12
+ import { type Scope, effectiveScope, withScope } from './scope';
12
13
  import { validateWorkflowFlow } from './workflow_validator';
13
14
  // Type-only import to break the circular `scenario.ts` ↔ `workflow.ts` graph at runtime.
14
15
  import type { Scenario } from './scenario';
@@ -32,6 +33,8 @@ type NodeType = DefinitionFlow['type'];
32
33
  */
33
34
  export class WorkflowEntity {
34
35
  /** @internal */ declare readonly _client: Scenario;
36
+ /** @internal scope captured from the retrieve call — replayed when `.run()` is invoked without an explicit query override. */
37
+ declare readonly _scope?: Scope;
35
38
 
36
39
  /** Find a node in the workflow definition by ID. */
37
40
  findNode(
@@ -59,22 +62,27 @@ export class WorkflowEntity {
59
62
  * `client.workflows.run(this.id, params)`.
60
63
  *
61
64
  * Returns an enhanced response where `response.job` has `.wait()`.
65
+ * If the workflow was fetched with a `projectId` scope (per-call override
66
+ * or client default), that scope is reused unless overridden by
67
+ * `options.query`.
62
68
  */
63
69
  run(
64
70
  this: WorkflowEntity & WorkflowRetrieveResponse.Workflow,
65
71
  params: WorkflowRunParams,
66
72
  options?: RequestOptions,
67
73
  ): APIPromise<WithJob<WorkflowRunResponse>> {
68
- return this._client.workflows.run(this.id, params, options);
74
+ return this._client.workflows.run(this.id, params, withScope(this._scope, options));
69
75
  }
70
76
 
71
- /** @internal Create a WorkflowEntity from raw workflow data. */
77
+ /** @internal Create a WorkflowEntity from raw workflow data, optionally remembering the originating scope. */
72
78
  static from(
73
79
  client: Scenario,
74
80
  data: WorkflowRetrieveResponse.Workflow,
81
+ scope?: Scope,
75
82
  ): WorkflowRetrieveResponse.Workflow & WorkflowEntity {
76
83
  const entity = Object.assign(Object.create(WorkflowEntity.prototype), data);
77
84
  Object.defineProperty(entity, '_client', { value: client, enumerable: false });
85
+ if (scope) Object.defineProperty(entity, '_scope', { value: scope, enumerable: false });
78
86
  return entity;
79
87
  }
80
88
  }
@@ -98,9 +106,11 @@ export class EnhancedWorkflows extends Workflows {
98
106
  workflowID: string,
99
107
  options?: RequestOptions,
100
108
  ): APIPromise<EnhancedWorkflowRetrieveResponse> {
109
+ const client = this._client as Scenario;
110
+ const scope = effectiveScope(options, client.projectId);
101
111
  return super.retrieve(workflowID, options)._thenUnwrap((data) => ({
102
112
  ...data,
103
- workflow: WorkflowEntity.from(this._client as Scenario, data.workflow),
113
+ workflow: WorkflowEntity.from(client, data.workflow, scope),
104
114
  }));
105
115
  }
106
116
 
@@ -110,7 +120,7 @@ export class EnhancedWorkflows extends Workflows {
110
120
  params: WorkflowRunParams,
111
121
  options?: RequestOptions,
112
122
  ): APIPromise<WithJob<WorkflowRunResponse>> {
113
- return enhanceJob(this._client, super.run(workflowID, params, options));
123
+ return enhanceJob(this._client, super.run(workflowID, params, options), options);
114
124
  }
115
125
 
116
126
  /** Approve a user approval node. Returns an enhanced response where `job` has `.wait()`. */
@@ -119,6 +129,6 @@ export class EnhancedWorkflows extends Workflows {
119
129
  body: WorkflowApproveUserParams,
120
130
  options?: RequestOptions,
121
131
  ): APIPromise<WithJob<WorkflowUserApprovalResponse>> {
122
- return enhanceJob(this._client, super.userApproval(workflowID, body, options));
132
+ return enhanceJob(this._client, super.userApproval(workflowID, body, options), options);
123
133
  }
124
134
  }
@@ -47,7 +47,7 @@ export type WorkflowEditorNode =
47
47
  }
48
48
  | { id: string; type: 'transformText'; data: NodeCommonData & { value?: string } }
49
49
  | { id: string; type: 'splitText'; data: NodeCommonData & { splitDelimiter?: string } }
50
- | { id: string; type: 'aspectRatio'; data: NodeCommonData & { output?: string } }
50
+ | { id: string; type: 'aspectRatio'; data: NodeCommonData & { output?: string; quality?: string } }
51
51
  | { id: string; type: 'groupItems'; data: NodeCommonData & { value?: string } }
52
52
  | {
53
53
  id: string;
@@ -61,8 +61,7 @@ export type WorkflowEditorNode =
61
61
  type: 'ifElse';
62
62
  data: NodeCommonData & { conditionBlocks?: WorkflowEditorConditionBlock[] };
63
63
  }
64
- | { id: string; type: 'approval'; data: NodeCommonData & { message?: string } }
65
- | { id: string; type: string; data: NodeCommonData };
64
+ | { id: string; type: 'approval'; data: NodeCommonData & { message?: string } };
66
65
 
67
66
  export type WorkflowEditorEdge = {
68
67
  source: string;
@@ -113,6 +112,7 @@ export type WorkflowEditorModel = {
113
112
 
114
113
  const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
115
114
  export const ASPECT_RATIO_PRESETS = [
115
+ '21:9',
116
116
  '16:9',
117
117
  '3:2',
118
118
  '4:3',
@@ -122,6 +122,7 @@ export const ASPECT_RATIO_PRESETS = [
122
122
  '3:4',
123
123
  '2:3',
124
124
  '9:16',
125
+ '9:21',
125
126
  ] as const;
126
127
 
127
128
  export type AspectRatioBounds = {
@@ -269,7 +270,13 @@ export function normalizeAspectRatio(aspectRatio: string | undefined): string |
269
270
  const parsed = parseAspectRatio(aspectRatio);
270
271
  if (!parsed) return undefined;
271
272
  const divisor = getGreatestCommonDivisor(parsed.width, parsed.height);
272
- return `${parsed.width / divisor}:${parsed.height / divisor}`;
273
+ const normalizedAspectRatio = `${parsed.width / divisor}:${parsed.height / divisor}`;
274
+ return (
275
+ {
276
+ '3:7': '9:21',
277
+ '7:3': '21:9',
278
+ }[normalizedAspectRatio] ?? normalizedAspectRatio
279
+ );
273
280
  }
274
281
 
275
282
  export function getAspectRatioModelInputNames(
@@ -652,9 +659,28 @@ function applyAspectRatioInputs(args: {
652
659
  flowId: string;
653
660
  aspectRatioRef: WorkflowEditorFlowInputRef;
654
661
  model: WorkflowEditorModel;
662
+ aspectRatioQuality: string | undefined;
655
663
  }): void {
656
664
  const modelInputNames = getAspectRatioModelInputNames(args.model);
657
- const modelBounds = getAspectRatioBounds(args.model, modelInputNames);
665
+ const rawModelBounds = getAspectRatioBounds(args.model, modelInputNames);
666
+ const quality = args.aspectRatioQuality;
667
+ const maxDimensionCap =
668
+ quality === 'high' ? 2048
669
+ : quality === 'medium' ? 1024
670
+ : undefined;
671
+ const isCapApplied =
672
+ rawModelBounds != null &&
673
+ maxDimensionCap != null &&
674
+ (rawModelBounds.maxWidth > maxDimensionCap || rawModelBounds.maxHeight > maxDimensionCap);
675
+ const modelBounds =
676
+ rawModelBounds && maxDimensionCap != null ?
677
+ {
678
+ ...rawModelBounds,
679
+ maxWidth: Math.min(rawModelBounds.maxWidth, maxDimensionCap),
680
+ maxHeight: Math.min(rawModelBounds.maxHeight, maxDimensionCap),
681
+ multipleOf: isCapApplied ? Math.min(rawModelBounds.multipleOf, 8) : rawModelBounds.multipleOf,
682
+ }
683
+ : rawModelBounds;
658
684
  const { widthInputName, heightInputName, aspectRatioInputName } = modelInputNames;
659
685
 
660
686
  if (aspectRatioInputName) {
@@ -1110,6 +1136,9 @@ export function convertWorkflowEditorToFlow(
1110
1136
 
1111
1137
  if (aspectRatioEdges.length > 0 && model) {
1112
1138
  const aspectRatioRef = getSourceRef(aspectRatioEdges[0]!, ctx);
1139
+ const aspectRatioNode = nodes.find((candidate) => candidate.id === aspectRatioEdges[0]!.target);
1140
+ const aspectRatioQuality =
1141
+ aspectRatioNode?.type === 'aspectRatio' ? aspectRatioNode.data.quality : undefined;
1113
1142
  if (aspectRatioRef) {
1114
1143
  applyAspectRatioInputs({
1115
1144
  flow,
@@ -1117,6 +1146,7 @@ export function convertWorkflowEditorToFlow(
1117
1146
  flowId,
1118
1147
  aspectRatioRef,
1119
1148
  model,
1149
+ aspectRatioQuality,
1120
1150
  });
1121
1151
  }
1122
1152
  }
@@ -1610,7 +1640,18 @@ export function convertWorkflowEditorToFlow(
1610
1640
 
1611
1641
  // Populate loopBodyNodeIds on each forEach flow item
1612
1642
  for (const [forEachId, forEachFlowItem] of forEachFlowItemRefs) {
1613
- forEachFlowItem.loopBodyNodeIds = forEachLoopFlowIds.get(forEachId) ?? [];
1643
+ const loopFlowIds = forEachLoopFlowIds.get(forEachId) ?? [];
1644
+ // Approval nodes attached to a node inside the loop body belong to the loop too,
1645
+ // but they are appended to the flow separately and never tracked above.
1646
+ const bodyEditorNodeIds = forEachBodyEditorNodeIds.get(forEachId);
1647
+ if (bodyEditorNodeIds) {
1648
+ for (const [approvedNodeId, approvalInfo] of approvalByApprovedNodeId) {
1649
+ if (bodyEditorNodeIds.has(approvedNodeId) && !loopFlowIds.includes(approvalInfo.approvalFlowId)) {
1650
+ loopFlowIds.push(approvalInfo.approvalFlowId);
1651
+ }
1652
+ }
1653
+ }
1654
+ forEachFlowItem.loopBodyNodeIds = loopFlowIds;
1614
1655
  }
1615
1656
 
1616
1657
  applyApprovalsToFlow({ flow, edges, approvalByApprovedNodeId, nodeIdToFlowIdMap });
@@ -18,7 +18,8 @@ export type WorkflowEditorNodeType =
18
18
  | 'sliceAssets'
19
19
  | 'forEach'
20
20
  | 'forEachEnd'
21
- | 'stickyNote';
21
+ | 'stickyNote'
22
+ | 'approval';
22
23
 
23
24
  export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
24
25
  'text',
@@ -34,6 +35,7 @@ export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
34
35
  'forEach',
35
36
  'forEachEnd',
36
37
  'stickyNote',
38
+ 'approval',
37
39
  ];
38
40
 
39
41
  export interface WorkflowExportData {