@sogni-ai/sogni-client 5.71.1 → 5.72.1

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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +28 -1
  3. package/dist/Account/index.d.ts +27 -2
  4. package/dist/Account/index.js +27 -2
  5. package/dist/Account/index.js.map +1 -1
  6. package/dist/Account/types.d.ts +10 -0
  7. package/dist/Chat/_hostedToolsManifest.generated.js +8 -0
  8. package/dist/Chat/_hostedToolsManifest.generated.js.map +1 -1
  9. package/dist/Projects/ReusableUploads.d.ts +1 -0
  10. package/dist/Projects/ReusableUploads.js +47 -18
  11. package/dist/Projects/ReusableUploads.js.map +1 -1
  12. package/dist/Projects/types/index.d.ts +4 -1
  13. package/dist/Projects/types/index.js.map +1 -1
  14. package/dist/index.d.ts +4 -4
  15. package/dist/index.js +3 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/types/ErrorData.d.ts +13 -0
  18. package/dist/types/ErrorData.js +14 -1
  19. package/dist/types/ErrorData.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.js +1 -1
  22. package/dist-esm/Account/index.js +27 -2
  23. package/dist-esm/Account/index.js.map +1 -1
  24. package/dist-esm/Chat/_hostedToolsManifest.generated.js +8 -0
  25. package/dist-esm/Chat/_hostedToolsManifest.generated.js.map +1 -1
  26. package/dist-esm/Projects/ReusableUploads.js +47 -18
  27. package/dist-esm/Projects/ReusableUploads.js.map +1 -1
  28. package/dist-esm/Projects/types/index.js.map +1 -1
  29. package/dist-esm/index.js +2 -2
  30. package/dist-esm/index.js.map +1 -1
  31. package/dist-esm/types/ErrorData.js +13 -0
  32. package/dist-esm/types/ErrorData.js.map +1 -1
  33. package/dist-esm/version.js +1 -1
  34. package/llms-full.txt +32 -4
  35. package/llms.txt +2 -1
  36. package/package.json +8 -4
  37. package/src/Account/index.ts +30 -2
  38. package/src/Account/types.ts +11 -0
  39. package/src/Chat/_hostedToolsManifest.generated.ts +8 -0
  40. package/src/Projects/ReusableUploads.ts +47 -19
  41. package/src/Projects/types/index.ts +4 -1
  42. package/src/index.ts +10 -1
  43. package/src/types/ErrorData.ts +14 -0
  44. package/src/version.ts +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  ClaimOptions,
6
6
  FullBalances,
7
7
  LoginData,
8
+ LoginOptions,
8
9
  MeData,
9
10
  Nonce,
10
11
  Reward,
@@ -459,18 +460,44 @@ class AccountApi extends ApiGroup {
459
460
  * console.log('Logged in');
460
461
  * ```
461
462
  *
463
+ * @example Offer to restore a deleted account at sign-in
464
+ * ```typescript
465
+ * import { ApiError, ACCOUNT_RESTORABLE_ERROR_CODE } from '@sogni-ai/sogni-client';
466
+ *
467
+ * try {
468
+ * await sogni.account.login(username, password, rememberMe, appSource);
469
+ * } catch (error) {
470
+ * if (!(error instanceof ApiError) || error.payload.errorCode !== ACCOUNT_RESTORABLE_ERROR_CODE) {
471
+ * throw error;
472
+ * }
473
+ * const deletedAt = new Date(Number(error.details?.deletedAt) * 1000);
474
+ * // Ask the person whether to restore the account deleted on `deletedAt`. If they agree,
475
+ * // repeat the original arguments and add the option:
476
+ * await sogni.account.login(username, password, rememberMe, appSource, {
477
+ * restoreDeletedAccount: true
478
+ * });
479
+ * }
480
+ * ```
481
+ *
462
482
  * @param username
463
483
  * @param password
464
484
  * @param rememberMe - Whether to establish a long-lived session. Default is false. Only
465
485
  * applicable for cookie-based authentication.
466
486
  * @param appSource - Optional client app/source label for login attribution. Defaults to the
467
487
  * SogniClient connection appSource when configured.
488
+ * @param options - Optional sign-in settings. Set `restoreDeletedAccount: true` to restore a
489
+ * deleted account and sign in, after the person confirms; repeat the original `rememberMe` and
490
+ * `appSource` on that call so the session lasts as long as first requested. A deleted account
491
+ * that can be restored makes `login()` throw an `ApiError` with
492
+ * `payload.errorCode === ACCOUNT_RESTORABLE_ERROR_CODE` (196) and `details.deletedAt` (Unix
493
+ * seconds).
468
494
  */
469
495
  async login(
470
496
  username: string,
471
497
  password: string,
472
498
  rememberMe = false,
473
- appSource?: string
499
+ appSource?: string,
500
+ options?: LoginOptions
474
501
  ): Promise<LoginData> {
475
502
  const wallet = this.getWallet(username, password);
476
503
  const nonce = await this.getNonce(wallet.address);
@@ -483,7 +510,8 @@ class AccountApi extends ApiGroup {
483
510
  walletAddress: wallet.address,
484
511
  signature,
485
512
  ...(resolvedAppSource ? { appSource: resolvedAppSource } : {}),
486
- rememberMe
513
+ rememberMe,
514
+ ...(options?.restoreDeletedAccount ? { restoreDeletedAccount: true } : {})
487
515
  });
488
516
  const auth = this.client.auth;
489
517
  if (auth instanceof TokenAuthManager) {
@@ -37,6 +37,17 @@ export interface LoginData {
37
37
  username: string;
38
38
  }
39
39
 
40
+ /** Optional settings for `sogni.account.login()`. */
41
+ export interface LoginOptions {
42
+ /**
43
+ * Restore the account, then sign in, when the account was deleted. Set it
44
+ * after the person confirms they want the account back, usually in answer to
45
+ * an `ACCOUNT_RESTORABLE_ERROR_CODE` error from an earlier `login()` call.
46
+ * It changes nothing for an account that has not been deleted.
47
+ */
48
+ restoreDeletedAccount?: boolean;
49
+ }
50
+
40
51
  export interface MeData {
41
52
  currentEmail: string;
42
53
  discord2FA: boolean;
@@ -326,6 +326,14 @@ export const SOGNI_HOSTED_TOOLS_MANIFEST: unknown = {
326
326
  "returnLastFrame": {
327
327
  "type": "boolean",
328
328
  "description": "Seedance 2.5 only. Set true to export a separate image of the final frame alongside the video. The result includes lastFrameUrl, which can be used as the first-frame image for a subsequent clip. Defaults to false; this does not extend the video automatically."
329
+ },
330
+ "referenceVideoCompression": {
331
+ "type": "string",
332
+ "enum": [
333
+ "none",
334
+ "vpack"
335
+ ],
336
+ "description": "Legacy compatibility preference for MiniMax H3 reference video. Current services select compression automatically; this field no longer selects compression. Leave it unset for new requests."
329
337
  }
330
338
  },
331
339
  "required": [
@@ -40,7 +40,8 @@ class AutomaticSaveBlockedError extends Error {}
40
40
 
41
41
  /** Private subscriber uploads that can be reused across projects. */
42
42
  export default class ReusableUploads {
43
- private pending = new Map<string, Promise<SavedUpload>>();
43
+ private pending = new Map<string, { upload: Promise<SavedUpload>; users: number }>();
44
+ private bindings = new Map<string, Promise<void>>();
44
45
  // Bound hashing/upload memory even when a project has many reference files.
45
46
  private lanes: Promise<unknown>[] = [Promise.resolve(), Promise.resolve()];
46
47
  private nextLane = 0;
@@ -60,6 +61,7 @@ export default class ReusableUploads {
60
61
  if (session !== this.lastSession) {
61
62
  this.lastSession = session;
62
63
  this.pending.clear();
64
+ this.bindings.clear();
63
65
  this.availability = undefined;
64
66
  this.automaticSaveBlockedUntil = 0;
65
67
  }
@@ -112,11 +114,24 @@ export default class ReusableUploads {
112
114
 
113
115
  async bind(id: string, binding: SavedUploadBinding): Promise<void> {
114
116
  const session = this.session;
115
- await this.retryBusy(() => {
116
- this.assertSession(session);
117
- return this.rest.post(`/v1/assets/${encodeURIComponent(id)}/bind`, { ...binding });
118
- });
119
- this.assertSession(session);
117
+ // The server holds a per-file lock while copying. Queue only this file;
118
+ // unrelated files still use the two upload lanes concurrently.
119
+ const previous = this.bindings.get(id) ?? Promise.resolve();
120
+ const operation = previous
121
+ .catch(() => undefined)
122
+ .then(async () => {
123
+ await this.retryBusy(() => {
124
+ this.assertSession(session);
125
+ return this.rest.post(`/v1/assets/${encodeURIComponent(id)}/bind`, { ...binding });
126
+ });
127
+ this.assertSession(session);
128
+ });
129
+ this.bindings.set(id, operation);
130
+ try {
131
+ await operation;
132
+ } finally {
133
+ if (this.bindings.get(id) === operation) this.bindings.delete(id);
134
+ }
120
135
  }
121
136
 
122
137
  private async retryBusy<T>(operation: () => Promise<T>): Promise<T> {
@@ -139,7 +154,8 @@ export default class ReusableUploads {
139
154
  file: Blob | Buffer,
140
155
  contentType: string,
141
156
  name: string,
142
- automatic: boolean
157
+ automatic: boolean,
158
+ binding?: SavedUploadBinding
143
159
  ): Promise<SavedUpload> {
144
160
  const session = this.session;
145
161
  const size = file instanceof Blob ? file.size : file.byteLength;
@@ -152,7 +168,7 @@ export default class ReusableUploads {
152
168
  })
153
169
  );
154
170
  const lane = this.nextLane++ % this.lanes.length;
155
- const run = this.lanes[lane].then(async () => {
171
+ const prepared = this.lanes[lane].then(async () => {
156
172
  this.assertSession(session);
157
173
  // Concurrent project helpers can all pass the capability check before
158
174
  // the first quota response arrives. Recheck once this lane reaches the
@@ -166,18 +182,30 @@ export default class ReusableUploads {
166
182
  value.toString(16).padStart(2, '0')
167
183
  ).join('');
168
184
  const key = `${sha256}:${contentType}`;
169
- const existing = this.pending.get(key);
170
- if (existing) return existing;
171
- const task = this.performUpload(file, bytes.byteLength, sha256, contentType, name, session);
172
- this.pending.set(key, task);
185
+ let pending = this.pending.get(key);
186
+ if (!pending) {
187
+ pending = {
188
+ upload: this.performUpload(file, bytes.byteLength, sha256, contentType, name, session),
189
+ users: 0
190
+ };
191
+ this.pending.set(key, pending);
192
+ }
193
+ pending.users += 1;
194
+ return { pending, key };
195
+ });
196
+ // The lane bounds hashing and transfer, not the per-file server copy.
197
+ this.lanes[lane] = prepared.then(({ pending }) => pending.upload).catch(() => undefined);
198
+ return prepared.then(async ({ pending, key }) => {
173
199
  try {
174
- return await task;
200
+ const saved = await pending.upload;
201
+ this.assertSession(session);
202
+ if (binding) await this.bind(saved.id, binding);
203
+ return saved;
175
204
  } finally {
176
- if (this.pending.get(key) === task) this.pending.delete(key);
205
+ pending.users -= 1;
206
+ if (pending.users === 0 && this.pending.get(key) === pending) this.pending.delete(key);
177
207
  }
178
208
  });
179
- this.lanes[lane] = run.catch(() => undefined);
180
- return run;
181
209
  }
182
210
 
183
211
  private async performUpload(
@@ -247,16 +275,16 @@ export default class ReusableUploads {
247
275
  this.assertSession(session);
248
276
  if (!enabled) return false;
249
277
  try {
250
- const saved = await this.uploadInternal(
278
+ await this.uploadInternal(
251
279
  file,
252
280
  contentType,
253
281
  file instanceof Blob && 'name' in file && typeof file.name === 'string'
254
282
  ? file.name
255
283
  : 'Saved upload',
256
- true
284
+ true,
285
+ binding
257
286
  );
258
287
  this.assertSession(session);
259
- await this.bind(saved.id, binding);
260
288
  return true;
261
289
  } catch (error) {
262
290
  this.assertSession(session);
@@ -717,7 +717,10 @@ export interface VideoProjectParams extends BaseProjectParams {
717
717
  * pricing and job admission. Omit this field when duration is unavailable.
718
718
  */
719
719
  referenceVideoDurations?: number[];
720
- /** Opt-in VPack compression for one-stage MiniMax H3 reference video. Omitted/none preserves the original reference tokens. */
720
+ /**
721
+ * Legacy MiniMax H3 reference-video preference, retained for compatibility.
722
+ * @deprecated Current services select compression automatically. Leave unset for new requests.
723
+ */
721
724
  referenceVideoCompression?: 'none' | 'vpack';
722
725
  /**
723
726
  * Inpaint mask IMAGE for distilled LTX 2.5 or LTX 2.3 v2v inpaint workflows.
package/src/index.ts CHANGED
@@ -120,7 +120,13 @@ import {
120
120
  isProjectLostError,
121
121
  projectParamsFromRecoveredProject
122
122
  } from './Projects/recovery.js';
123
- import type { Balances, Reward, RewardCantClaimReason, TxHistoryEntry } from './Account/types.js';
123
+ import type {
124
+ Balances,
125
+ LoginOptions,
126
+ Reward,
127
+ RewardCantClaimReason,
128
+ TxHistoryEntry
129
+ } from './Account/types.js';
124
130
  import type {
125
131
  CreateSubscriptionCheckoutOptions,
126
132
  SubscriptionCheckoutResult,
@@ -255,6 +261,7 @@ import {
255
261
  } from './Replay/types.js';
256
262
  // Base Types
257
263
  import ErrorData, {
264
+ ACCOUNT_RESTORABLE_ERROR_CODE,
258
265
  MODEL_CONSENT_REQUIRED_ERROR_CODE,
259
266
  MODEL_NOT_YET_AVAILABLE_ERROR_CODE,
260
267
  SUBSCRIPTION_ERROR_CODES
@@ -418,6 +425,7 @@ export type {
418
425
  // Primitives promoted from deep paths so consumers can import from the
419
426
  // package root rather than reaching into `./dist/*`.
420
427
  Balances,
428
+ LoginOptions,
421
429
  Reward,
422
430
  RewardCantClaimReason,
423
431
  TxHistoryEntry,
@@ -532,6 +540,7 @@ export {
532
540
  PROJECT_LOST_ORIGINAL_CODE,
533
541
  SogniTools,
534
542
  SUBSCRIPTION_ERROR_CODES,
543
+ ACCOUNT_RESTORABLE_ERROR_CODE,
535
544
  MODEL_CONSENT_REQUIRED_ERROR_CODE,
536
545
  MODEL_NOT_YET_AVAILABLE_ERROR_CODE,
537
546
  isModelConsentRequiredError,
@@ -98,6 +98,20 @@ export const MODEL_CONSENT_REQUIRED_ERROR_CODE = 4103;
98
98
  */
99
99
  export const MODEL_NOT_YET_AVAILABLE_ERROR_CODE = 4104;
100
100
 
101
+ /**
102
+ * REST error code `sogni.account.login()` throws when the account was deleted
103
+ * and signing in can restore it. It arrives as an `ApiError` with HTTP status
104
+ * 400 and `error.payload.errorCode` set to this code. `error.details.deletedAt`
105
+ * is when the account was deleted, in Unix seconds.
106
+ *
107
+ * Ask the person whether to restore the account. If they agree, call `login()`
108
+ * again with the same arguments, including `rememberMe` and `appSource`, plus
109
+ * `{ restoreDeletedAccount: true }`: the account is restored and the call signs
110
+ * in as usual. The error's `message` is readable as is for apps that do not
111
+ * handle this code.
112
+ */
113
+ export const ACCOUNT_RESTORABLE_ERROR_CODE = 196;
114
+
101
115
  /**
102
116
  * Socket error codes returned when a job explicitly submitted with
103
117
  * `billingMode: 'subscription'` cannot be covered by the subscription, plus
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/generate-version.mjs — do not edit.
2
2
  // Source of truth: package.json version.
3
- export const LIB_VERSION = '5.71.1';
3
+ export const LIB_VERSION = '5.72.1';