@somewhere-tech/cli 0.34.1 → 0.34.3

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.
@@ -186,17 +186,24 @@ var require_declared_data_contract = __commonJS({
186
186
  const names = /* @__PURE__ */ new Set();
187
187
  for (const relation of declared) {
188
188
  record(relation, `${table.name}.relation`);
189
- if (Object.keys(relation).some((key) => !["name", "table", "fk", "parentKey"].includes(key))) fail(`${table.name}.relation`);
190
- const { name, table: childName, fk, parentKey } = relation;
191
- if (![name, childName, fk, parentKey].every((value) => typeof value === "string" && identifier.test(value)) || names.has(name)) {
189
+ if (Object.keys(relation).some((key) => !["name", "kind", "table", "fk", "parentKey"].includes(key))) fail(`${table.name}.relation`);
190
+ const { name, kind, table: relatedName, fk, parentKey } = relation;
191
+ if (![name, relatedName, fk, parentKey].every((value) => typeof value === "string" && identifier.test(value)) || names.has(name)) {
192
192
  fail(`${table.name}.relation`);
193
193
  }
194
+ if (kind !== void 0 && kind !== "hasMany" && kind !== "belongsTo") fail(`${table.name}.relation`);
194
195
  names.add(name);
195
- const declaredChild = schema[childName];
196
- if (!declaredChild || !Array.isArray(declaredChild.columns) || parentKey !== table.primaryKey || !declaredChild.columns.some((column) => column && column.n === fk)) fail(`${table.name}.relation`);
197
- const child = tableByName.get(childName);
196
+ const related = schema[relatedName];
197
+ if (!related || !Array.isArray(related.columns)) fail(`${table.name}.relation`);
198
+ const declaring = schema[table.name];
199
+ if (kind === "belongsTo") {
200
+ if (!declaring.columns.some((column) => column && column.n === fk) || !related.columns.some((column) => column && column.n === parentKey)) fail(`${table.name}.relation`);
201
+ continue;
202
+ }
203
+ if (parentKey !== table.primaryKey || !related.columns.some((column) => column && column.n === fk)) fail(`${table.name}.relation`);
204
+ const child = tableByName.get(relatedName);
198
205
  if (table.client.read !== false && child && child.client.read !== false) {
199
- relations.push({ name, table: childName, fk, parentKey });
206
+ relations.push({ name, table: relatedName, fk, parentKey });
200
207
  }
201
208
  }
202
209
  if (relations.length) table.relations = relations.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
@@ -232,6 +239,20 @@ var require_declared_data_contract = __commonJS({
232
239
  operations.push(`list(options?: { limit?: number; after?: ${id}; where?: ${shape(columns, equality, "update")} }): Promise<{ data: ${row}[]; next: ${id} | null; has_more: boolean }>`);
233
240
  operations.push(`get(id: ${id}): Promise<{ data: ${row} | null }>`);
234
241
  methods.push("list", "get");
242
+ const numeric = equality.filter((field) => ["integer", "number"].includes(columns.find((column) => column.n === field).t));
243
+ const fieldUnion = (list) => list.map((field) => JSON.stringify(field)).join(" | ");
244
+ const aggregateOptions = ["count?: true"];
245
+ if (numeric.length) aggregateOptions.push(`sum?: ${fieldUnion(numeric)}`, `avg?: ${fieldUnion(numeric)}`);
246
+ if (equality.length) {
247
+ aggregateOptions.push(`min?: ${fieldUnion(equality)}`, `max?: ${fieldUnion(equality)}`);
248
+ aggregateOptions.push(`groupBy?: Array<${fieldUnion(equality)}>`);
249
+ }
250
+ aggregateOptions.push(`where?: ${shape(columns, equality, "update")}`);
251
+ aggregateOptions.push("having?: Record<string, { eq?: number; ne?: number; lt?: number; lte?: number; gt?: number; gte?: number }>");
252
+ aggregateOptions.push(`order?: string | [string, 'asc' | 'desc'] | Array<[string, 'asc' | 'desc']>`);
253
+ aggregateOptions.push("limit?: number");
254
+ operations.push(`aggregate(options: { ${aggregateOptions.join("; ")} }): Promise<{ data: Array<Record<string, number | string | boolean | null>>; has_more?: boolean }>`);
255
+ methods.push("aggregate");
235
256
  }
236
257
  const mutation = client.read === false ? "{ count: number; changes: number }" : `{ data: ${row} | null; count: number; changes: number }`;
237
258
  if (client.create !== null) {
@@ -272,7 +293,7 @@ var require_declared_data_contract = __commonJS({
272
293
  const runtime = `const contract=${JSON.stringify(contract_digest)};
273
294
  class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
274
295
  const invoke=async(table,operation,input)=>{const response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"},body:JSON.stringify({...input,contract,table,operation})});const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
275
- const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"?(options={})=>invoke(table,operation,options):operation==="create"?values=>invoke(table,operation,{values}):operation==="update"?(id,values)=>invoke(table,operation,{id,values}):id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];})));
296
+ const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?values=>invoke(table,operation,{values}):operation==="update"?(id,values)=>invoke(table,operation,{id,values}):id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];})));
276
297
  export{data,DataError};
277
298
  `;
278
299
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration } };
@@ -352,6 +373,7 @@ function shared(): SomewhereSchemaDeclaration.Scope;
352
373
  function serverOnly(): SomewhereSchemaDeclaration.Scope;
353
374
  function member(options: { group: string | string[]; membership: string; member_user: string; member_group: string | string[]; operations?: Array<'read' | 'create' | 'update' | 'delete'> }): SomewhereSchemaDeclaration.MemberScope;
354
375
  function hasMany(table: string, foreignKey: string): SomewhereSchemaDeclaration.Relation;
376
+ function belongsTo(table: string, foreignKey: string): SomewhereSchemaDeclaration.Relation;
355
377
  function removed(): SomewhereSchemaDeclaration.ColumnMarker;
356
378
  function removedTable(): SomewhereSchemaDeclaration.TableMarker;
357
379
  function exported(): SomewhereSchemaDeclaration.TableMarker;
@@ -418,8 +440,9 @@ type SomewhereDbWriteIntent =
418
440
  | ({ op: 'update'; table: string } & SomewhereDbUpdate)
419
441
  | { op: 'remove'; table: string; where?: SomewhereDbWhere | null };
420
442
  interface SomewhereDbResult {
421
- // Composed results preserve the database representation. They do not use
422
- // the browser client's boolean/JSON normalization or its field projection.
443
+ // Composed results return db/schema.ts's declared types, as the browser data
444
+ // client does: boolean columns as true/false, json columns as their value,
445
+ // blob columns as byte arrays. Raw query/batch rows are exactly as stored.
423
446
  data: Record<string, unknown>[];
424
447
  error: null;
425
448
  count: number;
@@ -428,7 +451,18 @@ interface SomewhereDbResult {
428
451
  live_delivery?: { delivery: 'invalidated' }
429
452
  | { delivery: 'resync_required'; reason: string };
430
453
  }
454
+ interface SomewhereRawDbStatement {
455
+ sql: string;
456
+ params?: readonly unknown[];
457
+ }
458
+ interface SomewhereRawBatchResult {
459
+ data: Record<string, unknown>[];
460
+ changes: number;
461
+ last_row_id: number | string | null;
462
+ }
431
463
  interface SomewhereServerDb {
464
+ query(sql: string, params?: readonly unknown[]): Promise<SomewhereDbResult>;
465
+ batch(statements: readonly SomewhereRawDbStatement[]): Promise<SomewhereRawBatchResult[]>;
432
466
  from(table: string, options?: SomewhereDbReadOptions | null): Promise<SomewhereDbResult>;
433
467
  count(table: string, options?: SomewhereDbCountOptions | null): Promise<{ data: number; error: null }>;
434
468
  insert(table: string, values: SomewhereDbValues, options?: SomewhereDbInsertOptions | null): Promise<SomewhereDbResult>;
@@ -445,9 +479,2273 @@ interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count'> {
445
479
  delete(table: string, spec?: SomewhereDbRemove | null): Promise<SomewhereDbResult>;
446
480
  readonly server: SomewhereServerDb;
447
481
  }
448
- // Deliberately describes the composed database subset. Other sw namespaces
449
- // are not inferred from code or copied from the unrelated developer SDK.
450
- interface SomewhereRuntimeContext { readonly db: SomewhereCallerDb }
482
+ interface SomewhereAuthUser {
483
+ id: string;
484
+ email: string;
485
+ role?: string;
486
+ display_name?: string | null;
487
+ [key: string]: unknown;
488
+ }
489
+ interface SomewhereAuthCredentials { email: string; password: string }
490
+ type SomewhereCookieLoginResult =
491
+ | { user: SomewhereAuthUser; mfa_required?: never; mfa_token?: never }
492
+ | { mfa_required: true; mfa_token: string; user?: never };
493
+ type SomewhereAuthSession = {
494
+ user: SomewhereAuthUser;
495
+ refresh_token: string;
496
+ session_token?: string;
497
+ expires_in?: number;
498
+ } & ({ token: string; access_token?: string } | { token?: string; access_token: string });
499
+ interface SomewhereRuntimeAuth {
500
+ loginWithCookie(req: Request, email: string, password: string): Promise<SomewhereCookieLoginResult>;
501
+ loginWithCookie(req: Request, credentials: SomewhereAuthCredentials): Promise<SomewhereCookieLoginResult>;
502
+ loginWithCookie(credentials: SomewhereAuthCredentials): Promise<SomewhereCookieLoginResult>;
503
+ setSessionCookies(access: string, refresh: string): void;
504
+ readonly mfa: SomewhereRuntimeAuthMfa;
505
+ }
506
+ // Every binding the runtime exports to a deployed function, typed from the
507
+ // runtime implementation and the routes behind it (runtime-types.test.mjs holds
508
+ // the declaration to the runtime's own context builder). Tombstones that always
509
+ // throw stay off the type so their failure moves to typecheck.
510
+ interface SomewhereRuntimeContext {
511
+ readonly db: SomewhereCallerDb;
512
+ readonly auth: SomewhereRuntimeAuth;
513
+ readonly project_id: string;
514
+ readonly subdomain: string;
515
+ readonly tier: string;
516
+ // The project's environment variables (set with somewhere env).
517
+ readonly env: Readonly<Record<string, string>>;
518
+ readonly request_id: string;
519
+ readonly trace: { readonly id: string | null; readonly span_id: string | null };
520
+ // Route parameters from a dynamic file name such as api/rounds/[id].ts.
521
+ readonly params: Readonly<Record<string, string>>;
522
+ // Legacy aliases of this same object.
523
+ readonly sw: SomewhereRuntimeContext;
524
+ readonly ctx: SomewhereRuntimeContext;
525
+ }
526
+ // Shared by every namespace: a JSON object whose fields are unknown until narrowed.
527
+ type SomewhereJsonObject = { [key: string]: unknown };
528
+ // ---- sw.db extras: aggregate, live, scope, tables, caller raw options ----
529
+ // sw.db.migrate / sw.db.dump / sw.db.onchange are deliberately undeclared: they always throw in a deployed function.
530
+ type __SomewhereDbHavingOperators = { eq: string | number | boolean; ne: string | number | boolean; lt: string | number | boolean; lte: string | number | boolean; gt: string | number | boolean; gte: string | number | boolean };
531
+ type SomewhereDbHavingCondition = {
532
+ [K in keyof __SomewhereDbHavingOperators]: Pick<__SomewhereDbHavingOperators, K>
533
+ & Partial<Record<Exclude<keyof __SomewhereDbHavingOperators, K>, never>>
534
+ }[keyof __SomewhereDbHavingOperators];
535
+ // At least one of count/sum/avg/min/max; count takes no column, the others name one column.
536
+ type SomewhereDbAggregateMeasures = {
537
+ count?: true | null; sum?: string | null; avg?: string | null; min?: string | null; max?: string | null;
538
+ } & ({ count: true } | { sum: string } | { avg: string } | { min: string } | { max: string });
539
+ type SomewhereDbAggregateOptions = SomewhereDbAggregateMeasures & {
540
+ groupBy?: readonly string[] | null;
541
+ where?: SomewhereDbWhere | null;
542
+ has?: Readonly<Record<string, SomewhereDbWhere>> | null;
543
+ // Keys are this call's result keys: count, sum_<col>, avg_<col>, min_<col>, max_<col>.
544
+ having?: Readonly<Record<string, SomewhereDbHavingCondition | readonly SomewhereDbHavingCondition[]>> | null;
545
+ // May name only a groupBy column or a result key.
546
+ order?: SomewhereDbOrder | null;
547
+ limit?: number | null;
548
+ };
549
+ interface SomewhereDbAggregateResult {
550
+ // Always an array of group rows (exactly one when ungrouped): grouped columns plus composed result keys.
551
+ data: Record<string, unknown>[];
552
+ error: null;
553
+ // Number of rows (groups) returned; the COUNT(*) value lives in each row under "count".
554
+ count: number;
555
+ }
556
+ // Legacy raw-SQL annotations: accepted and ignored, they confer no authority. { user } throws.
557
+ interface SomewhereDbRawCallerOptions { unscoped?: true | null; asServer?: true | null; user?: never }
558
+ type SomewhereDbLiveState =
559
+ | { name: string; state: 'ready'; release_id?: string; fingerprint: string; subscribe_url: string; expires_at: number }
560
+ | { name: string; state: 'resync_required'; reason: string };
561
+ type SomewhereDbLiveResult = SomewhereDbResult & { live: SomewhereDbLiveState };
562
+ type SomewhereDbScopeDeclaration =
563
+ | { intent?: 'scoped' | null; owner_column: string; sensitive_columns?: readonly string[] | null }
564
+ | { intent: 'shared' | 'server_only'; owner_column?: string | null; sensitive_columns?: readonly string[] | null };
565
+ interface SomewhereDbScopeEntry {
566
+ table: string;
567
+ owner_column: string | null;
568
+ intent: 'scoped' | 'shared' | 'server_only' | 'member' | 'policy';
569
+ sensitive_columns: string[];
570
+ created_at: string;
571
+ }
572
+ interface SomewhereDbScopeActivation {
573
+ status: 'rebaked' | 'partial' | 'stale' | 'no_functions';
574
+ slots: SomewhereJsonObject[];
575
+ rebaked: SomewhereJsonObject[];
576
+ stale: SomewhereJsonObject[];
577
+ }
578
+ type SomewhereDbScopeDeclareResult =
579
+ | {
580
+ saved?: never;
581
+ project_id: string;
582
+ table: string;
583
+ owner_column: string | null;
584
+ intent: 'scoped' | 'shared' | 'server_only';
585
+ sensitive_columns: string[];
586
+ scope_activation: SomewhereDbScopeActivation;
587
+ scope_warnings: { code: string; message: string }[];
588
+ }
589
+ // Saved, but activates only on the next deploy or publish.
590
+ | { saved: true; activates_on: 'next_deploy_or_publish' };
591
+ interface SomewhereDbScopeApi {
592
+ (table: string, options: SomewhereDbScopeDeclaration): Promise<SomewhereDbScopeDeclareResult>;
593
+ get(table: string): Promise<SomewhereDbScopeEntry | null>;
594
+ list(): Promise<SomewhereDbScopeEntry[]>;
595
+ }
596
+ interface SomewhereServerDb {
597
+ aggregate(table: string, options: SomewhereDbAggregateOptions): Promise<SomewhereDbAggregateResult>;
598
+ }
599
+ interface SomewhereCallerDb {
600
+ query(sql: string, params?: readonly unknown[] | null, options?: SomewhereDbRawCallerOptions | null): Promise<SomewhereDbResult>;
601
+ batch(statements: readonly SomewhereRawDbStatement[], options?: SomewhereDbRawCallerOptions | null): Promise<SomewhereRawBatchResult[]>;
602
+ aggregate(table: string, options: SomewhereDbAggregateOptions & { asServer?: true }): Promise<SomewhereDbAggregateResult>;
603
+ // read must be the exact sw.db.from(...) result (or its promise) this release declares under name.
604
+ live(name: string, read: SomewhereDbResult | PromiseLike<SomewhereDbResult>): Promise<SomewhereDbLiveResult>;
605
+ tables(): Promise<string[]>;
606
+ readonly scope: SomewhereDbScopeApi;
607
+ }
608
+
609
+ // ---- sw.postgres: the @neondatabase/serverless 1.1.0 neon() callable for the attached database ----
610
+ // Unattached projects get the same shape; every call throws POSTGRES_NOT_ATTACHED.
611
+ type __SomewherePostgresRows<ArrayMode extends boolean> = ArrayMode extends true ? unknown[][] : Record<string, unknown>[];
612
+ interface SomewherePostgresFullResults<ArrayMode extends boolean> {
613
+ fields: { name: string; tableID: number; columnID: number; dataTypeID: number; dataTypeSize: number; dataTypeModifier: number; format: string }[];
614
+ command: string;
615
+ rowCount: number;
616
+ rows: __SomewherePostgresRows<ArrayMode>;
617
+ rowAsArray: ArrayMode;
618
+ }
619
+ type __SomewherePostgresResult<ArrayMode extends boolean, FullResults extends boolean> =
620
+ FullResults extends true ? SomewherePostgresFullResults<ArrayMode> : __SomewherePostgresRows<ArrayMode>;
621
+ interface SomewherePostgresQueryOptions<ArrayMode extends boolean, FullResults extends boolean> {
622
+ arrayMode?: ArrayMode;
623
+ fullResults?: FullResults;
624
+ fetchOptions?: Readonly<Record<string, unknown>>;
625
+ authToken?: string | (() => Promise<string> | string);
626
+ types?: { getTypeParser: (...args: never[]) => unknown };
627
+ disableWarningInBrowsers?: boolean;
628
+ }
629
+ interface SomewherePostgresTransactionOptions<ArrayMode extends boolean, FullResults extends boolean>
630
+ extends SomewherePostgresQueryOptions<ArrayMode, FullResults> {
631
+ isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
632
+ readOnly?: boolean;
633
+ deferrable?: boolean;
634
+ }
635
+ // A pending driver query: awaitable, and accepted by transaction([...]) because it carries its queryData.
636
+ interface SomewherePostgresQuery<T> extends Promise<T> { readonly queryData: unknown }
637
+ interface SomewherePostgresUnsafeRawSql { sql: string }
638
+ interface SomewherePostgresInTransaction {
639
+ (strings: TemplateStringsArray, ...params: unknown[]): SomewherePostgresQuery<Record<string, unknown>[]>;
640
+ query(text: string, params?: readonly unknown[]): SomewherePostgresQuery<Record<string, unknown>[]>;
641
+ unsafe(rawSql: string): SomewherePostgresUnsafeRawSql;
642
+ }
643
+ interface SomewhereRuntimePostgres {
644
+ (strings: TemplateStringsArray, ...params: unknown[]): SomewherePostgresQuery<Record<string, unknown>[]>;
645
+ query<ArrayMode extends boolean = false, FullResults extends boolean = false>(
646
+ text: string, params?: readonly unknown[], options?: SomewherePostgresQueryOptions<ArrayMode, FullResults>,
647
+ ): SomewherePostgresQuery<__SomewherePostgresResult<ArrayMode, FullResults>>;
648
+ // Embeds a trusted raw SQL fragment inside a tagged template; never pass user input.
649
+ unsafe(rawSql: string): SomewherePostgresUnsafeRawSql;
650
+ // Non-interactive: the statement list is fixed before the call and applied atomically.
651
+ transaction<ArrayMode extends boolean = false, FullResults extends boolean = false>(
652
+ queries: readonly SomewherePostgresQuery<unknown>[] | ((sql: SomewherePostgresInTransaction) => readonly SomewherePostgresQuery<unknown>[]),
653
+ options?: SomewherePostgresTransactionOptions<ArrayMode, FullResults>,
654
+ ): Promise<__SomewherePostgresResult<ArrayMode, FullResults>[]>;
655
+ }
656
+ interface SomewhereRuntimeContext { readonly postgres: SomewhereRuntimePostgres }
657
+ // ---- sw.auth (worker/src/runtime/auth.ts) + sw.crypto (worker/src/runtime/crypto.ts) ----
658
+ type SomewhereAuthRole = 'user' | 'admin';
659
+ // The user a sign-in route returns (signup/login/OTP/OAuth/MFA challenge).
660
+ interface SomewhereAuthSignedInUser extends SomewhereAuthUser {
661
+ role: string;
662
+ display_name: string | null;
663
+ }
664
+ // The verified profile /v1/auth/me returns; fromRequest/requireUser return it (plus enrichFrom columns; platform fields win).
665
+ interface SomewhereAuthProfileUser extends SomewhereAuthUser {
666
+ role: SomewhereAuthRole;
667
+ display_name: string | null;
668
+ email_verified: boolean;
669
+ banned: boolean;
670
+ metadata: unknown;
671
+ plan: string;
672
+ plan_status: string | null;
673
+ type: string;
674
+ created_at: number;
675
+ last_login_at: number | null;
676
+ locale?: string | null;
677
+ timezone?: string | null;
678
+ entitlements: string[];
679
+ agent?: SomewhereJsonObject;
680
+ }
681
+ interface SomewhereAuthIssuedSession {
682
+ user: SomewhereAuthSignedInUser;
683
+ token: string;
684
+ access_token: string;
685
+ refresh_token: string;
686
+ session_token: string;
687
+ expires_in: number;
688
+ mfa_required?: never;
689
+ mfa_token?: never;
690
+ }
691
+ interface SomewhereAuthMfaRequired {
692
+ mfa_required: true;
693
+ mfa_token: string;
694
+ user?: never;
695
+ token?: never;
696
+ access_token?: never;
697
+ refresh_token?: never;
698
+ }
699
+ type SomewhereAuthLoginResult = SomewhereAuthIssuedSession | SomewhereAuthMfaRequired;
700
+ interface SomewhereAuthOtpSession extends SomewhereAuthIssuedSession {
701
+ // The redirect_uri passed to signInWithOtp, or null.
702
+ redirect_uri: string | null;
703
+ }
704
+ interface SomewhereAuthOAuthSession {
705
+ user: SomewhereAuthSignedInUser;
706
+ token: string;
707
+ refresh_token: string;
708
+ session_token?: string;
709
+ }
710
+ interface SomewhereAuthGoogleSession {
711
+ user: SomewhereAuthSignedInUser;
712
+ token: string;
713
+ refresh_token?: string;
714
+ session_token?: string;
715
+ }
716
+ interface SomewhereAuthRefreshed {
717
+ access_token: string;
718
+ refresh_token: string;
719
+ expires_in: number;
720
+ token_type: 'Bearer';
721
+ }
722
+ interface SomewhereAuthSignupProfile {
723
+ display_name?: string | null;
724
+ displayName?: string | null;
725
+ full_name?: string | null;
726
+ fullName?: string | null;
727
+ name?: string | null;
728
+ locale?: string;
729
+ timezone?: string;
730
+ turnstile_token?: string;
731
+ }
732
+ interface SomewhereAuthSignupOptions extends SomewhereAuthSignupProfile { email: string; password: string }
733
+ type SomewhereAuthLogoutOptions =
734
+ | { session_token: string; refresh_token?: string }
735
+ | { session_token?: string; refresh_token: string };
736
+ interface SomewhereAuthMessage { message: string }
737
+ type SomewhereAuthOtpSent = SomewhereAuthMessage | { status: 'pending'; message_id: string; message: string };
738
+ type SomewhereAuthVerificationSent =
739
+ | { already_verified: true }
740
+ | { sent: true; code_created: true; expires_in_seconds: number }
741
+ | { sent: boolean; code_created: false; pending: true; code: 'EMAIL_SEND_PENDING'; message: string };
742
+ interface SomewhereAuthProfileUpdate {
743
+ display_name?: string | null;
744
+ // Replaces the stored metadata blob (JSON, max 16 KB); null clears it.
745
+ metadata?: __SomewhereJson;
746
+ email?: never;
747
+ }
748
+ interface SomewhereAuthEmailChangeRequest {
749
+ email: string;
750
+ current_password?: string;
751
+ display_name?: never;
752
+ metadata?: never;
753
+ }
754
+ interface SomewhereAuthUpdatedUser extends SomewhereAuthUser {
755
+ role: SomewhereAuthRole;
756
+ display_name: string | null;
757
+ email_verified: boolean;
758
+ created_at: number;
759
+ last_login_at: number | null;
760
+ metadata: unknown;
761
+ }
762
+ interface SomewhereAuthEmailChange {
763
+ id: string;
764
+ new_email: string;
765
+ phase: string;
766
+ pending?: true;
767
+ expires_in_seconds?: number;
768
+ }
769
+ type SomewhereAuthEmailChangeStarted = { email_change: SomewhereAuthEmailChange } & SomewhereJsonObject;
770
+ // Options for fromRequest / requireUser: optional enrichment join and an explicitly forwarded app-user token.
771
+ interface SomewhereAuthRequestOptions {
772
+ enrichFrom?: string;
773
+ fields?: readonly string[];
774
+ on?: string;
775
+ // A platform-issued app-user access token string; never an identity value.
776
+ forwardedToken?: string | null;
777
+ }
778
+ interface SomewhereAuthRequireOptions extends SomewhereAuthRequestOptions { role?: 'admin' }
779
+ interface SomewhereAuthAnonSession { id: string; isAnon: true; expiresAt: number }
780
+ interface SomewhereAuthModerationRevoked { revoked_sessions: number; revoked_refresh_tokens: number }
781
+ interface SomewhereRuntimeAuthModeration {
782
+ ban(req: Request, userId: string, options?: { reason?: string | null } | null): Promise<SomewhereAuthModerationRevoked & {
783
+ user: { id: string; email: string; banned: true; banned_reason: string | null };
784
+ }>;
785
+ unban(req: Request, userId: string): Promise<{ user: { id: string; email: string; banned: false } }>;
786
+ deleteUser(req: Request, userId: string): Promise<{ deleted: true; user_id: string }>;
787
+ revokeSessions(req: Request, userId: string): Promise<SomewhereAuthModerationRevoked>;
788
+ setRole(req: Request, userId: string, role: SomewhereAuthRole): Promise<SomewhereAuthModerationRevoked & {
789
+ user: { id: string; email: string; role: SomewhereAuthRole };
790
+ }>;
791
+ }
792
+ // Replaces the inline mfa type in SomewhereRuntimeAuth (challenge/challengeWithCookie unchanged).
793
+ interface SomewhereRuntimeAuthMfa {
794
+ challenge(options: { mfa_token: string; code: string }): Promise<SomewhereAuthSession>;
795
+ challengeWithCookie(options: { mfa_token: string; code: string }): Promise<{ user: SomewhereAuthUser }>;
796
+ enroll(options: { token: string }): Promise<{
797
+ secret: string; enrollment_id: string; otpauth_uri: string; issuer: string; account: string;
798
+ }>;
799
+ reauthenticate(options: { token: string; enrollment_id: string; method: 'password'; password: string }):
800
+ Promise<{ method: 'password'; activation_token: string; expires_in_seconds: number }>;
801
+ reauthenticate(options: { token: string; enrollment_id: string; method: 'email' }):
802
+ Promise<{ method: 'email'; challenge_id: string; expires_in_seconds: number; pending?: true }>;
803
+ verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }):
804
+ Promise<{ method: 'email'; activation_token: string; expires_in_seconds: number }>;
805
+ verify(options: { token: string; enrollment_id: string; activation_token: string; code: string }):
806
+ Promise<{ enabled: true; backup_codes: string[] }>;
807
+ unenroll(options: { token: string; code: string }): Promise<{ enabled: false }>;
808
+ }
809
+ interface SomewhereRuntimeAuth {
810
+ signup(options: SomewhereAuthSignupOptions): Promise<SomewhereAuthIssuedSession>;
811
+ login(options: SomewhereAuthCredentials): Promise<SomewhereAuthLoginResult>;
812
+ logout(options: SomewhereAuthLogoutOptions): Promise<{ logged_out: true }>;
813
+ refresh(options: { refresh_token: string }): Promise<SomewhereAuthRefreshed>;
814
+ // Verifies an app-user access token; throws on an invalid/expired token (no null result).
815
+ me(token: string, options?: { refreshToken?: string | null; noCache?: boolean } | null): Promise<{ user: SomewhereAuthProfileUser }>;
816
+ // Resolves the signed-in user from cookie or Bearer (or options.forwardedToken); null when absent/rejected.
817
+ fromRequest(req: Request | null, options?: SomewhereAuthRequestOptions | null): Promise<SomewhereAuthProfileUser | null>;
818
+ // Throws 401 AUTH_REQUIRED when signed out; role 'admin' throws 403 FORBIDDEN for non-admins.
819
+ requireUser(req: Request | null, options?: SomewhereAuthRequireOptions | null): Promise<SomewhereAuthProfileUser>;
820
+ requireRole(req: Request | null, role: 'admin', options?: SomewhereAuthRequestOptions | null): Promise<SomewhereAuthProfileUser>;
821
+ forgot(options: { email: string }): Promise<SomewhereAuthMessage>;
822
+ reset(options: { token: string; new_password: string }): Promise<SomewhereAuthMessage>;
823
+ requestEmailVerification(token: string): Promise<SomewhereAuthVerificationSent>;
824
+ resendVerification(token: string): Promise<SomewhereAuthVerificationSent>;
825
+ verifyEmail(token: string, options: { code: string }): Promise<{ verified: true }>;
826
+ updatePassword(token: string, options: { new_password: string; current_password?: string }): Promise<{ updated: true }>;
827
+ updateProfile(token: string, options: SomewhereAuthProfileUpdate): Promise<{ user: SomewhereAuthUpdatedUser }>;
828
+ // Email change is verify-gated: sends a code, never writes the address immediately.
829
+ updateProfile(token: string, options: SomewhereAuthEmailChangeRequest): Promise<SomewhereAuthEmailChangeStarted>;
830
+ updateProfileWithCookie(req: Request, options: SomewhereAuthProfileUpdate): Promise<{ user: SomewhereAuthUpdatedUser }>;
831
+ deleteUser(token: string): Promise<{ deleted: true }>;
832
+ googleUrl(options: { redirect_uri: string }): string;
833
+ githubUrl(options: { redirect_uri: string }): string;
834
+ discordUrl(options: { redirect_uri: string }): string;
835
+ googleExchange(options: { code: string; redirect_uri?: string }): Promise<SomewhereAuthGoogleSession>;
836
+ githubExchange(options: { code: string }): Promise<SomewhereAuthOAuthSession>;
837
+ discordExchange(options: { code: string }): Promise<SomewhereAuthOAuthSession>;
838
+ // Reads ?code, exchanges it, stages the session cookies, returns a 302 to redirectTo (default '/').
839
+ googleCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
840
+ githubCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
841
+ discordCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
842
+ signupWithCookie(req: Request, email: string, password: string, options?: SomewhereAuthSignupProfile | string | null): Promise<{ user: SomewhereAuthUser }>;
843
+ signupWithCookie(req: Request, options: SomewhereAuthSignupOptions): Promise<{ user: SomewhereAuthUser }>;
844
+ signupWithCookie(options: SomewhereAuthSignupOptions): Promise<{ user: SomewhereAuthUser }>;
845
+ clearSessionCookies(): void;
846
+ logoutWithCookie(req: Request): Promise<{ ok: true }>;
847
+ signInWithOtp(options: { email: string; redirect_uri?: string }): Promise<SomewhereAuthOtpSent>;
848
+ verifyOtp(options: { token: string }): Promise<SomewhereAuthOtpSession>;
849
+ anonSession(): Promise<SomewhereAuthAnonSession>;
850
+ readonly moderation: SomewhereRuntimeAuthModeration;
851
+ }
852
+ interface SomewhereRuntimeCrypto {
853
+ // Lowercase hex HMAC-SHA256 of message keyed by secret.
854
+ hmacSha256Hex(message: string, secret: string): Promise<string>;
855
+ timingSafeEqual(a: string, b: string): boolean;
856
+ readonly bcrypt: {
857
+ // Verify-only; there is no hashing surface.
858
+ verify(password: string, hash: string): Promise<boolean>;
859
+ };
860
+ }
861
+ interface SomewhereRuntimeContext { readonly crypto: SomewhereRuntimeCrypto }
862
+ // ---- sw.payments / sw.quote / sw.connect / sw.billing / sw.calendar ----
863
+ type SomewherePaymentsEnv = 'dev' | 'prod';
864
+ type SomewherePaymentsStripeMode = 'test' | 'live';
865
+
866
+ // Quote input (sw.quote / sw.payments.quote). Amounts are integer cents.
867
+ type SomewhereQuoteFeeKind = 'flat' | 'per_night' | 'per_guest' | 'per_guest_per_night';
868
+ interface SomewhereQuoteRateCalendarEntry { start?: string; end?: string; amount?: number; label?: string; weekdays?: readonly number[] }
869
+ interface SomewhereQuoteBaseRate {
870
+ amount?: number;
871
+ unit?: 'flat' | 'night';
872
+ calendar?: readonly SomewhereQuoteRateCalendarEntry[];
873
+ rate_calendar?: readonly SomewhereQuoteRateCalendarEntry[];
874
+ weekday?: Readonly<Record<string, number>>;
875
+ weekdays?: Readonly<Record<string, number>>;
876
+ }
877
+ interface SomewhereQuoteFee { name?: string; label?: string; amount?: number; kind?: SomewhereQuoteFeeKind; type?: SomewhereQuoteFeeKind; taxable?: boolean }
878
+ interface SomewhereQuoteDiscountSpec {
879
+ name?: string; label?: string; code?: string; promo?: string;
880
+ amount?: number; percent?: number; percent_bps?: number;
881
+ min_nights?: number; minNights?: number; min_guests?: number; minGuests?: number;
882
+ }
883
+ interface SomewhereQuoteTaxSpec { rate_bps?: number; rate?: number; stripe?: boolean; tax_code?: string; tax_behavior?: 'exclusive' | 'inclusive' }
884
+ interface SomewhereQuoteDepositSpec { amount?: number; percent?: number; percent_bps?: number; capture_method?: 'manual' | 'automatic' }
885
+ interface SomewhereQuoteResource {
886
+ id?: string;
887
+ name?: string;
888
+ currency?: string;
889
+ baseRate?: number | SomewhereQuoteBaseRate;
890
+ base_rate?: number | SomewhereQuoteBaseRate;
891
+ rate?: number | SomewhereQuoteBaseRate;
892
+ rateCalendar?: readonly SomewhereQuoteRateCalendarEntry[];
893
+ rate_calendar?: readonly SomewhereQuoteRateCalendarEntry[];
894
+ fees?: readonly SomewhereQuoteFee[];
895
+ discounts?: readonly SomewhereQuoteDiscountSpec[];
896
+ promo_codes?: readonly SomewhereQuoteDiscountSpec[];
897
+ promoCodes?: readonly SomewhereQuoteDiscountSpec[];
898
+ tax?: SomewhereQuoteTaxSpec;
899
+ deposit?: number | SomewhereQuoteDepositSpec;
900
+ }
901
+ // Date-only 'YYYY-MM-DD' values, 1 to 366 nights.
902
+ type SomewhereQuoteRange = { start?: string; end?: string; from?: string; to?: string } | readonly [string, string];
903
+ interface SomewhereQuoteOptions {
904
+ env?: SomewherePaymentsEnv;
905
+ guests?: number;
906
+ promo?: string;
907
+ // Required when resource.tax.stripe is true; fields follow Stripe Tax calculation params.
908
+ tax?: { customer_details?: SomewhereJsonObject; ship_from_details?: SomewhereJsonObject; tax_date?: number };
909
+ booking?: {
910
+ id?: string;
911
+ external_id?: string;
912
+ externalId?: string;
913
+ // Derived from the signed-in app user; a different or subjectless value is refused.
914
+ app_user_id?: string;
915
+ appUserId?: string;
916
+ metadata?: SomewhereJsonObject;
917
+ };
918
+ }
919
+ interface SomewhereQuoteLineItem {
920
+ id: string;
921
+ kind: 'base' | 'fee' | 'tax' | 'deposit';
922
+ name: string;
923
+ amount: number;
924
+ currency: string;
925
+ quantity: number;
926
+ taxable: boolean;
927
+ source?: string;
928
+ }
929
+ interface SomewhereQuoteResult {
930
+ quote_id: string;
931
+ booking_id: string;
932
+ expires_at: string;
933
+ resource: { id: string | null; name: string };
934
+ range: { start: string; end: string; nights: number };
935
+ guests: number;
936
+ promo: string | null;
937
+ currency: string;
938
+ subtotal: number;
939
+ taxes: { amount: number; source: 'none' | 'local' | 'stripe_tax'; calculation_id?: string | null; breakdown?: unknown };
940
+ deposit: { amount: number; capture_method: 'manual' | 'automatic' };
941
+ total: number;
942
+ lineItems: SomewhereQuoteLineItem[];
943
+ discounts: { name: string; amount: number; source: 'tier' | 'promo' }[];
944
+ }
945
+ type SomewhereQuoteFunction = (resource: SomewhereQuoteResource, range: SomewhereQuoteRange, opts?: SomewhereQuoteOptions | null) => Promise<SomewhereQuoteResult>;
946
+
947
+ // Payments
948
+ interface SomewherePaymentsOnboardOptions { return_url?: string; refresh_url?: string; mode?: SomewherePaymentsStripeMode; env?: SomewherePaymentsEnv }
949
+ interface SomewherePaymentsOnboardResult {
950
+ account_id: string;
951
+ test_account_id: string;
952
+ onboarding_url: string | null;
953
+ expires_at: string | null;
954
+ mode: SomewherePaymentsStripeMode;
955
+ test_only: boolean;
956
+ // Present on test-mode onboarding only.
957
+ charges_enabled?: boolean;
958
+ checkout_ready?: boolean;
959
+ platform_fee_percent: number;
960
+ platform_fee_note: string | null;
961
+ }
962
+ type SomewherePaymentsStatus =
963
+ | { connected: false; onboarded: false; charges_enabled: false; payouts_enabled: false }
964
+ | {
965
+ connected: boolean;
966
+ account_id: string | null;
967
+ test_account_id: string | null;
968
+ onboarded: boolean;
969
+ charges_enabled: boolean;
970
+ payouts_enabled: boolean;
971
+ details_submitted: boolean;
972
+ country: string | null;
973
+ default_currency: string | null;
974
+ };
975
+ interface SomewherePaymentsLineItem { price?: string; amount?: number; currency?: string; name?: string; quantity?: number }
976
+ interface SomewherePaymentsCheckoutOptions {
977
+ env?: SomewherePaymentsEnv;
978
+ mode?: 'payment' | 'subscription';
979
+ line_items?: readonly SomewherePaymentsLineItem[];
980
+ quote_id?: string;
981
+ booking_id?: string;
982
+ calendar_hold_token?: string;
983
+ // Catalog plan slug (sw.billing); resolves its stripe_price_id when line_items is empty.
984
+ plan?: string;
985
+ success_url: string;
986
+ cancel_url: string;
987
+ customer_email?: string;
988
+ metadata?: Readonly<Record<string, string>>;
989
+ }
990
+ interface SomewherePaymentsCheckoutForUserOptions extends SomewherePaymentsCheckoutOptions { plan: string }
991
+ interface SomewherePaymentsCheckoutResult {
992
+ session_id: string;
993
+ url: string | null;
994
+ amount_total_cents: number;
995
+ platform_fee_cents: number;
996
+ fee_percent: number;
997
+ stripe_mode: SomewherePaymentsStripeMode;
998
+ is_stand_in: boolean;
999
+ checkout_intent_id: string | null;
1000
+ quote_id: string | null;
1001
+ booking_id: string | null;
1002
+ capture_method: 'manual' | 'automatic';
1003
+ payment_intent_id: string | null;
1004
+ }
1005
+ type SomewherePaymentsRefundOptions = {
1006
+ amount?: number;
1007
+ reason?: 'requested_by_customer' | 'duplicate' | 'fraudulent';
1008
+ env?: SomewherePaymentsEnv;
1009
+ idempotency_key?: string;
1010
+ } & ({ payment_intent_id: string; charge_id?: string } | { payment_intent_id?: string; charge_id: string });
1011
+ interface SomewherePaymentsRefundResult {
1012
+ refund_id: string | null;
1013
+ status: string | null;
1014
+ amount: number | null;
1015
+ currency: string | null;
1016
+ charge_id: string | null;
1017
+ payment_intent_id: string | null;
1018
+ stripe_mode: SomewherePaymentsStripeMode;
1019
+ refund_intent_id: string;
1020
+ intent_status: 'pending' | 'succeeded' | 'failed' | 'unknown';
1021
+ duplicate?: true;
1022
+ }
1023
+ interface SomewherePaymentsCancelSubscriptionOptions { subscription_id: string; immediately?: boolean; env?: SomewherePaymentsEnv }
1024
+ interface SomewherePaymentsCancelSubscriptionResult {
1025
+ subscription_id: string;
1026
+ status: string;
1027
+ cancel_at_period_end: boolean;
1028
+ canceled_at: string | null;
1029
+ current_period_end: string | null;
1030
+ stripe_mode: SomewherePaymentsStripeMode;
1031
+ }
1032
+ interface SomewherePaymentsTransactionsOptions { limit?: number; starting_after?: string | null; startingAfter?: string | null; env?: SomewherePaymentsEnv }
1033
+ interface SomewherePaymentsTransaction {
1034
+ id: string;
1035
+ amount: number;
1036
+ amount_refunded: number;
1037
+ currency: string;
1038
+ status: string;
1039
+ paid: boolean;
1040
+ refunded: boolean;
1041
+ created: string;
1042
+ customer_id: string | null;
1043
+ payment_intent_id: string | null;
1044
+ receipt_email: string | null;
1045
+ description: string | null;
1046
+ }
1047
+ interface SomewherePaymentsTransactionsResult {
1048
+ transactions: SomewherePaymentsTransaction[];
1049
+ has_more: boolean;
1050
+ next_cursor: string | null;
1051
+ stripe_mode: SomewherePaymentsStripeMode;
1052
+ }
1053
+ type SomewherePaymentsPortalOptions = {
1054
+ return_url?: string;
1055
+ returnUrl?: string;
1056
+ env?: SomewherePaymentsEnv;
1057
+ } & ({ customer_id: string; customerId?: string } | { customer_id?: string; customerId: string });
1058
+ interface SomewherePaymentsPortalForUserOptions { return_url?: string; returnUrl?: string; env?: SomewherePaymentsEnv }
1059
+ interface SomewherePaymentsPortalResult { url: string; stripe_mode: SomewherePaymentsStripeMode }
1060
+ interface SomewherePaymentsEventsOptions { limit?: number; before?: number | null; type?: string }
1061
+ interface SomewherePaymentsEvent {
1062
+ id: string;
1063
+ type: string;
1064
+ mode: string;
1065
+ account_id: string | null;
1066
+ project_id: string | null;
1067
+ amount_cents: number | null;
1068
+ currency: string | null;
1069
+ livemode: boolean;
1070
+ received_at: string;
1071
+ }
1072
+ // next_cursor is an epoch-ms number; pass it back as before.
1073
+ interface SomewherePaymentsEventsResult { events: SomewherePaymentsEvent[]; next_cursor: number | null }
1074
+ interface SomewhereRuntimePayments {
1075
+ onboard(opts?: SomewherePaymentsOnboardOptions | null): Promise<SomewherePaymentsOnboardResult>;
1076
+ quote: SomewhereQuoteFunction;
1077
+ status(opts?: { refresh?: boolean } | null): Promise<SomewherePaymentsStatus>;
1078
+ checkout(opts: SomewherePaymentsCheckoutOptions): Promise<SomewherePaymentsCheckoutResult>;
1079
+ // The app user is the request's verified principal; no user id argument.
1080
+ checkoutForUser(opts: SomewherePaymentsCheckoutForUserOptions): Promise<SomewherePaymentsCheckoutResult>;
1081
+ dashboardLink(): Promise<{ url: string }>;
1082
+ refund(opts: SomewherePaymentsRefundOptions): Promise<SomewherePaymentsRefundResult>;
1083
+ cancelSubscription(opts: SomewherePaymentsCancelSubscriptionOptions): Promise<SomewherePaymentsCancelSubscriptionResult>;
1084
+ transactions(opts?: SomewherePaymentsTransactionsOptions | null): Promise<SomewherePaymentsTransactionsResult>;
1085
+ portal(opts: SomewherePaymentsPortalOptions): Promise<SomewherePaymentsPortalResult>;
1086
+ // The app user is the request's verified principal; no user id argument.
1087
+ portalForUser(opts: SomewherePaymentsPortalForUserOptions): Promise<SomewherePaymentsPortalResult>;
1088
+ events(opts?: SomewherePaymentsEventsOptions | null): Promise<SomewherePaymentsEventsResult>;
1089
+ }
1090
+
1091
+ // Connect (read-only link to a creator's existing Stripe account)
1092
+ interface SomewhereConnectStripeStatus {
1093
+ connected: boolean;
1094
+ account_id: string | null;
1095
+ scope: string | null;
1096
+ status: string;
1097
+ sync_error: string | null;
1098
+ connected_at: number | null;
1099
+ updated_at: number | null;
1100
+ }
1101
+ interface SomewhereConnectStripeSubscriber {
1102
+ email: string;
1103
+ stripe_customer_id: string | null;
1104
+ subscription_id: string | null;
1105
+ status: string | null;
1106
+ price_id: string | null;
1107
+ product_id: string | null;
1108
+ tier: string | null;
1109
+ current_period_end: number | null;
1110
+ amount: number | null;
1111
+ currency: string | null;
1112
+ }
1113
+ interface SomewhereConnectStripeSubscribersResult {
1114
+ data: SomewhereConnectStripeSubscriber[];
1115
+ next_cursor: string | null;
1116
+ sync_status: 'connected';
1117
+ updated_at: number | null;
1118
+ }
1119
+ interface SomewhereRuntimeConnectStripe {
1120
+ connect(opts?: { return_url?: string; returnUrl?: string } | null): Promise<{ url: string }>;
1121
+ status(): Promise<SomewhereConnectStripeStatus>;
1122
+ subscribers(opts?: { status?: string; limit?: number; cursor?: string | null } | null): Promise<SomewhereConnectStripeSubscribersResult>;
1123
+ disconnect(): Promise<{ ok: true }>;
1124
+ }
1125
+ interface SomewhereRuntimeConnect { readonly stripe: SomewhereRuntimeConnectStripe }
1126
+
1127
+ // Billing (plan catalog + entitlements for the app's own users)
1128
+ interface SomewhereBillingPlanFeatureInput { feature: string; limit?: number | null }
1129
+ interface SomewhereBillingPlanInput {
1130
+ slug: string;
1131
+ name: string;
1132
+ description?: string | null;
1133
+ price_cents?: number | null;
1134
+ currency?: string;
1135
+ interval?: 'month' | 'year' | null;
1136
+ stripe_price_id?: string | null;
1137
+ sort_order?: number;
1138
+ is_default?: boolean;
1139
+ active?: boolean;
1140
+ features?: readonly (string | SomewhereBillingPlanFeatureInput)[];
1141
+ }
1142
+ interface SomewhereBillingPlan {
1143
+ slug: string;
1144
+ name: string;
1145
+ description: string | null;
1146
+ price_cents: number | null;
1147
+ currency: string;
1148
+ interval: string | null;
1149
+ stripe_price_id: string | null;
1150
+ sort_order: number;
1151
+ is_default: boolean;
1152
+ active: boolean;
1153
+ features: { feature: string; limit_value: number | null }[];
1154
+ }
1155
+ interface SomewhereBillingDefinePlansResult { plans: SomewhereBillingPlan[]; retained_plan_slugs: string[]; warning: string | null }
1156
+ interface SomewhereBillingEntitlements { plan: string; plan_defined: boolean; features: string[]; limits: Record<string, number> }
1157
+ interface SomewhereRuntimeBilling {
1158
+ definePlans(plans: readonly SomewhereBillingPlanInput[]): Promise<SomewhereBillingDefinePlansResult>;
1159
+ plans(): Promise<{ plans: SomewhereBillingPlan[] }>;
1160
+ // Subject is the request's verified principal; takes only the feature slug.
1161
+ has(feature: string): Promise<boolean>;
1162
+ entitlements(): Promise<SomewhereBillingEntitlements>;
1163
+ }
1164
+
1165
+ // Calendar. Instants are epoch ms or ISO strings with an explicit Z/offset.
1166
+ type SomewhereCalendarInstant = string | number;
1167
+ interface SomewhereCalendarRange { start: SomewhereCalendarInstant; end: SomewhereCalendarInstant; timezone: string }
1168
+ interface SomewhereCalendarReadRange { start: SomewhereCalendarInstant; end: SomewhereCalendarInstant }
1169
+ interface SomewhereCalendarRebookRange { start: SomewhereCalendarInstant; end: SomewhereCalendarInstant; timezone?: string | null }
1170
+ type SomewhereCalendarReservationStatus = 'hold' | 'pending_payment' | 'confirmed' | 'payment_received_slot_lost' | 'released' | 'expired';
1171
+ interface SomewhereCalendarReservation {
1172
+ id: string;
1173
+ project_id: string;
1174
+ resource: string;
1175
+ start_ms: number;
1176
+ end_ms: number;
1177
+ timezone: string;
1178
+ kind: 'reservation' | 'blackout';
1179
+ status: SomewhereCalendarReservationStatus;
1180
+ hold_token_prefix: string;
1181
+ metadata: unknown;
1182
+ payment_reference: string | null;
1183
+ expires_at: number | null;
1184
+ confirmed_at: number | null;
1185
+ released_at: number | null;
1186
+ release_reason: string | null;
1187
+ cancellation_status: 'complete_no_refund' | 'refund_pending' | 'refund_succeeded' | 'refund_failed' | 'refund_unknown' | null;
1188
+ cancellation_refund_intent_id: string | null;
1189
+ cancellation_refund_error: string | null;
1190
+ created_at: number;
1191
+ updated_at: number;
1192
+ }
1193
+ interface SomewhereCalendarHoldOptions {
1194
+ resource: string;
1195
+ range: SomewhereCalendarRange;
1196
+ // ttl and ttl_seconds are seconds; ttl_ms wins when set. Default 35 min, max 24 h.
1197
+ ttl?: number;
1198
+ ttl_seconds?: number;
1199
+ ttl_ms?: number;
1200
+ metadata?: SomewhereJsonObject | null;
1201
+ }
1202
+ interface SomewhereCalendarHoldResult { token: string; hold_token: string; expires_at: number; reservation: SomewhereCalendarReservation }
1203
+ interface SomewhereCalendarBlackoutExtras { reason?: string | null; metadata?: SomewhereJsonObject | null }
1204
+ interface SomewhereCalendarBlackoutOptions extends SomewhereCalendarBlackoutExtras { resource: string; range: SomewhereCalendarRange }
1205
+ type __SomewhereCalendarIdRef = { reservation_id: string } | { booking_id: string } | { id: string };
1206
+ type SomewhereCalendarRemoveBlackoutOptions = ({ reservation_id: string } | { blackout_id: string } | { id: string }) & { reason?: string | null };
1207
+ interface SomewhereCalendarRefundPolicy { full_before_days: number; partial_percent: number }
1208
+ interface SomewhereCalendarCancelExtras { reason?: string | null; refund_policy?: SomewhereCalendarRefundPolicy | null }
1209
+ type SomewhereCalendarCancelOptions = __SomewhereCalendarIdRef & SomewhereCalendarCancelExtras;
1210
+ type SomewhereCalendarCancelRefund =
1211
+ | { refund_id: string; status: string | null; amount: number; currency: string; charge_id: string | null; payment_intent_id: string | null; stripe_mode: SomewherePaymentsStripeMode }
1212
+ | { status: 'not_due'; amount: 0; currency: string };
1213
+ interface SomewhereCalendarCancelResult { canceled: boolean; reservation: SomewhereCalendarReservation; refund: SomewhereCalendarCancelRefund | null }
1214
+ type SomewhereCalendarRebookOptions = __SomewhereCalendarIdRef & ({ new_range: SomewhereCalendarRebookRange } | { range: SomewhereCalendarRebookRange });
1215
+ // Each field is optional; null clears it.
1216
+ interface SomewhereCalendarPolicyInput {
1217
+ min_stay_nights?: number | null;
1218
+ max_stay_nights?: number | null;
1219
+ advance_notice_hours?: number | null;
1220
+ bookable_from?: SomewhereCalendarInstant | null;
1221
+ turnaround_hours?: number | null;
1222
+ }
1223
+ type SomewhereCalendarSetPolicyOptions = { resource: string } & ({ policy: SomewhereCalendarPolicyInput } | (SomewhereCalendarPolicyInput & { policy?: never }));
1224
+ interface SomewhereCalendarPolicy {
1225
+ resource: string;
1226
+ min_stay_nights: number | null;
1227
+ max_stay_nights: number | null;
1228
+ advance_notice_hours: number | null;
1229
+ bookable_from_ms: number | null;
1230
+ turnaround_hours: number | null;
1231
+ created_at: number | null;
1232
+ updated_at: number | null;
1233
+ }
1234
+ interface SomewhereCalendarAvailabilityResult {
1235
+ resource: string;
1236
+ range: { start_ms: number; end_ms: number };
1237
+ busy: {
1238
+ reservation_id: string;
1239
+ kind: 'hold' | 'booking' | 'blackout';
1240
+ status: 'hold' | 'pending_payment' | 'confirmed';
1241
+ start_ms: number;
1242
+ end_ms: number;
1243
+ expires_at: number | null;
1244
+ }[];
1245
+ free: { start_ms: number; end_ms: number }[];
1246
+ }
1247
+ interface SomewhereCalendarListExtras {
1248
+ range?: SomewhereCalendarReadRange | null;
1249
+ // Defaults to the active statuses (hold, pending_payment, confirmed).
1250
+ statuses?: SomewhereCalendarReservationStatus | readonly SomewhereCalendarReservationStatus[] | null;
1251
+ limit?: number | null;
1252
+ cursor?: string | null;
1253
+ }
1254
+ interface SomewhereCalendarListOptions extends SomewhereCalendarListExtras { resource: string }
1255
+ interface SomewhereCalendarListResult { reservations: SomewhereCalendarReservation[]; next_cursor: string | null }
1256
+ interface SomewhereCalendarTokenExtras { payment_reference?: string | null }
1257
+ interface SomewhereCalendarReleaseExtras extends SomewhereCalendarTokenExtras { release_reason?: string | null }
1258
+ type __SomewhereCalendarTokenRef = { token: string } | { hold_token: string };
1259
+ interface SomewhereRuntimeCalendar {
1260
+ hold(opts: SomewhereCalendarHoldOptions): Promise<SomewhereCalendarHoldResult>;
1261
+ hold(resource: string, range: SomewhereCalendarRange, ttl?: number): Promise<SomewhereCalendarHoldResult>;
1262
+ blackout(opts: SomewhereCalendarBlackoutOptions): Promise<{ blackout: SomewhereCalendarReservation }>;
1263
+ blackout(resource: string, range: SomewhereCalendarRange, opts?: SomewhereCalendarBlackoutExtras | null): Promise<{ blackout: SomewhereCalendarReservation }>;
1264
+ removeBlackout(opts: SomewhereCalendarRemoveBlackoutOptions): Promise<{ removed: boolean; blackout: SomewhereCalendarReservation | null }>;
1265
+ removeBlackout(reservationId: string, opts?: { reason?: string | null } | null): Promise<{ removed: boolean; blackout: SomewhereCalendarReservation | null }>;
1266
+ cancel(opts: SomewhereCalendarCancelOptions): Promise<SomewhereCalendarCancelResult>;
1267
+ cancel(reservationId: string, opts?: SomewhereCalendarCancelExtras | null): Promise<SomewhereCalendarCancelResult>;
1268
+ rebook(opts: SomewhereCalendarRebookOptions): Promise<{ rebooked: boolean; reservation: SomewhereCalendarReservation }>;
1269
+ rebook(reservationId: string, newRange: SomewhereCalendarRebookRange): Promise<{ rebooked: boolean; reservation: SomewhereCalendarReservation }>;
1270
+ setPolicy(opts: SomewhereCalendarSetPolicyOptions): Promise<{ policy: SomewhereCalendarPolicy }>;
1271
+ setPolicy(resource: string, policy: SomewhereCalendarPolicyInput): Promise<{ policy: SomewhereCalendarPolicy }>;
1272
+ getPolicy(resourceOrOpts: string | { resource: string }): Promise<{ policy: SomewhereCalendarPolicy }>;
1273
+ availability(opts: { resource: string; range: SomewhereCalendarReadRange }): Promise<SomewhereCalendarAvailabilityResult>;
1274
+ availability(resource: string, range: SomewhereCalendarReadRange): Promise<SomewhereCalendarAvailabilityResult>;
1275
+ list(opts: SomewhereCalendarListOptions): Promise<SomewhereCalendarListResult>;
1276
+ list(resource: string, opts?: SomewhereCalendarListExtras | null): Promise<SomewhereCalendarListResult>;
1277
+ get(idOrOpts: string | __SomewhereCalendarIdRef): Promise<{ reservation: SomewhereCalendarReservation }>;
1278
+ pending(opts: __SomewhereCalendarTokenRef & SomewhereCalendarTokenExtras): Promise<{ reservation: SomewhereCalendarReservation }>;
1279
+ pending(token: string, opts?: SomewhereCalendarTokenExtras | null): Promise<{ reservation: SomewhereCalendarReservation }>;
1280
+ confirm(opts: __SomewhereCalendarTokenRef & SomewhereCalendarTokenExtras): Promise<{ reservation: SomewhereCalendarReservation }>;
1281
+ confirm(token: string, opts?: SomewhereCalendarTokenExtras | null): Promise<{ reservation: SomewhereCalendarReservation }>;
1282
+ release(opts: __SomewhereCalendarTokenRef & SomewhereCalendarReleaseExtras): Promise<{ released: boolean; reservation: SomewhereCalendarReservation | null }>;
1283
+ release(token: string, opts?: SomewhereCalendarReleaseExtras | null): Promise<{ released: boolean; reservation: SomewhereCalendarReservation | null }>;
1284
+ expire(opts?: { resource?: string | null } | null): Promise<{ expired: number }>;
1285
+ }
1286
+
1287
+ interface SomewhereRuntimeContext {
1288
+ readonly payments: SomewhereRuntimePayments;
1289
+ readonly connect: SomewhereRuntimeConnect;
1290
+ // Same operation as sw.payments.quote.
1291
+ readonly quote: SomewhereQuoteFunction;
1292
+ readonly billing: SomewhereRuntimeBilling;
1293
+ readonly calendar: SomewhereRuntimeCalendar;
1294
+ }
1295
+ // ---- files + utilities: sw.fs, sw.search, sw.image, sw.render, sw.web, sw.fetch, sw.rateLimit, sw.logs, sw.analytics
1296
+
1297
+ // Body shape a platform route returns when it refuses a request.
1298
+ interface SomewhereFsErrorEnvelope {
1299
+ ok: false;
1300
+ error: string;
1301
+ message: string;
1302
+ code?: string;
1303
+ retry?: boolean;
1304
+ retry_after_ms?: number;
1305
+ hint?: string;
1306
+ data?: SomewhereJsonObject;
1307
+ }
1308
+ type SomewhereFsVisibility = 'public' | 'private';
1309
+ type SomewhereFsOwnerSubjectType = 'app_user' | 'project_owner';
1310
+ type SomewhereFsWriteBody = string | ArrayBuffer | ArrayBufferView | Blob | ReadableStream<Uint8Array>;
1311
+ interface SomewhereFsWriteOptions {
1312
+ contentType?: string;
1313
+ content_type?: string;
1314
+ visibility?: SomewhereFsVisibility;
1315
+ public?: boolean;
1316
+ // Refuse with FS_VERSION_CONFLICT when the file's current version differs.
1317
+ ifMatch?: number | string | null;
1318
+ if_match?: number | string | null;
1319
+ }
1320
+ interface SomewhereFsWriteData {
1321
+ path: string;
1322
+ size_bytes: number;
1323
+ content_type: string;
1324
+ version: number;
1325
+ content_revision: string;
1326
+ }
1327
+ // write resolves with the full route envelope; read fields off .data.
1328
+ interface SomewhereFsWriteResult { ok: true; data: SomewhereFsWriteData }
1329
+ interface SomewhereFsReadLinesOptions { lines: string | readonly [number, number] }
1330
+ interface SomewhereFsReadOptions { lines?: string | readonly [number, number] | null }
1331
+ interface SomewhereFsReadLinesResult {
1332
+ path: string;
1333
+ content: string;
1334
+ lines: [number, number];
1335
+ total_lines: number;
1336
+ content_type: string | null;
1337
+ version: number;
1338
+ content_revision: string | null;
1339
+ }
1340
+ interface SomewhereFsDeleteData { deleted: number; type: 'file' | 'directory'; path: string }
1341
+ // delete does not throw on a refused request; it resolves with the error body.
1342
+ type SomewhereFsDeleteResult = { ok: true; data: SomewhereFsDeleteData } | SomewhereFsErrorEnvelope;
1343
+ interface SomewhereFsMoveOptions { overwrite?: boolean }
1344
+ interface SomewhereFsMoveResult { from: string; to: string }
1345
+ interface SomewhereFsCopyResult { from: string; to: string; content_revision: string }
1346
+ interface SomewhereFsRestoreResult {
1347
+ path: string;
1348
+ restored_version: number;
1349
+ current_version: number;
1350
+ content_revision: string;
1351
+ }
1352
+ interface SomewhereFsStat {
1353
+ path: string;
1354
+ name: string;
1355
+ type: 'file' | 'directory';
1356
+ size_bytes: number;
1357
+ content_type: string | null;
1358
+ content_revision: string | null;
1359
+ visibility: SomewhereFsVisibility;
1360
+ version: number;
1361
+ created_at: string;
1362
+ updated_at: string;
1363
+ }
1364
+ interface SomewhereFsVersion { version: number; size_bytes: number; content_type: string; created_at: string }
1365
+ interface SomewhereFsVersionsResult { path: string; current_version: number; versions: SomewhereFsVersion[] }
1366
+ interface SomewhereFsListOptions { recursive?: boolean; depth?: number }
1367
+ interface SomewhereFsListEntry {
1368
+ path: string;
1369
+ name: string;
1370
+ type: 'file' | 'directory';
1371
+ size_bytes: number;
1372
+ content_type: string | null;
1373
+ version: number;
1374
+ updated_at: string;
1375
+ }
1376
+ type SomewhereFsListResult =
1377
+ | { path: string; type: 'directory'; entries: SomewhereFsListEntry[]; next_cursor: string | null; recursive?: never }
1378
+ | { path: string; type: 'directory'; entries: SomewhereFsListEntry[]; recursive: true; depth: number | null };
1379
+ interface SomewhereFsReplaceOptions { path: string; find: string; replace: string }
1380
+ interface SomewhereFsReplaceResult {
1381
+ ok: true;
1382
+ replacements: number;
1383
+ path: string;
1384
+ version: number;
1385
+ size_bytes: number;
1386
+ content_revision?: string;
1387
+ }
1388
+ interface SomewhereFsUploadUrlOptions {
1389
+ path: string;
1390
+ maxSize?: number;
1391
+ max_size?: number;
1392
+ contentType?: string;
1393
+ content_type?: string;
1394
+ expiresIn?: number;
1395
+ expires_in?: number;
1396
+ }
1397
+ interface SomewhereFsUploadUrlResult {
1398
+ url: string;
1399
+ multipart_url: string;
1400
+ path: string;
1401
+ expires_at: string;
1402
+ max_size: number;
1403
+ content_type: string;
1404
+ public: boolean;
1405
+ owner_subject_type: SomewhereFsOwnerSubjectType;
1406
+ owner_subject_id: string;
1407
+ }
1408
+ interface SomewhereFsSignedUrlOptions { expiresIn?: number; expires_in?: number }
1409
+ interface SomewhereFsSignedUrlResult { url: string; token: string; path: string; expires_at: string; expires_in: number }
1410
+ interface SomewhereFsPublicUrlOptions { makePublic?: boolean; make_public?: boolean; public?: boolean }
1411
+ interface SomewhereFsPublicUrlResult {
1412
+ path: string;
1413
+ public_url: string;
1414
+ content_type: string | null;
1415
+ size_bytes: number;
1416
+ visibility: 'public';
1417
+ }
1418
+ interface SomewhereFsSetOwnerResult {
1419
+ path: string;
1420
+ owner_subject_type: SomewhereFsOwnerSubjectType;
1421
+ owner_subject_id: string;
1422
+ }
1423
+ interface SomewhereFsUploadFromRequestOptions {
1424
+ path: string;
1425
+ maxBytes?: number;
1426
+ max_bytes?: number;
1427
+ allowedTypes?: readonly string[];
1428
+ fieldName?: string;
1429
+ field_name?: string;
1430
+ public?: boolean;
1431
+ visibility?: SomewhereFsVisibility;
1432
+ }
1433
+ type SomewhereFsUploadFromRequestResult =
1434
+ | { url: string; path: string; size: number; contentType: string; visibility: 'public' }
1435
+ | { url: string | null; path: string; size: number; contentType: string; visibility: 'private' };
1436
+ // Shared by sw.fs (acting user derived from the request) and sw.fs.server (project-wide).
1437
+ // The project-wide scanners diff/glob/search exist only on sw.fs.dev in run_code.
1438
+ interface SomewhereFsView {
1439
+ read(path: string, options: SomewhereFsReadLinesOptions): Promise<SomewhereFsReadLinesResult>;
1440
+ read(path: string, options?: { lines?: null } | null): Promise<Response>;
1441
+ read(path: string, options?: SomewhereFsReadOptions | null): Promise<Response | SomewhereFsReadLinesResult>;
1442
+ write(path: string, body: SomewhereFsWriteBody, options?: SomewhereFsWriteOptions | null): Promise<SomewhereFsWriteResult>;
1443
+ delete(path: string): Promise<SomewhereFsDeleteResult>;
1444
+ move(from: string, to: string, options?: SomewhereFsMoveOptions | null): Promise<SomewhereFsMoveResult>;
1445
+ copy(from: string, to: string): Promise<SomewhereFsCopyResult>;
1446
+ restore(path: string, version: number): Promise<SomewhereFsRestoreResult>;
1447
+ stat(path: string): Promise<SomewhereFsStat>;
1448
+ versions(path: string): Promise<SomewhereFsVersionsResult>;
1449
+ list(path?: string | null, options?: SomewhereFsListOptions | null): Promise<SomewhereFsListResult>;
1450
+ replace(options: SomewhereFsReplaceOptions): Promise<SomewhereFsReplaceResult>;
1451
+ uploadUrl(options: SomewhereFsUploadUrlOptions): Promise<SomewhereFsUploadUrlResult>;
1452
+ signedUrl(path: string, options?: SomewhereFsSignedUrlOptions | null): Promise<SomewhereFsSignedUrlResult>;
1453
+ public_url(path: string, options?: SomewhereFsPublicUrlOptions | null): Promise<SomewhereFsPublicUrlResult>;
1454
+ publicUrl(path: string, options?: SomewhereFsPublicUrlOptions | null): Promise<SomewhereFsPublicUrlResult>;
1455
+ // null resets ownership to the project.
1456
+ setOwner(path: string, user: string | { readonly id: string } | null): Promise<SomewhereFsSetOwnerResult>;
1457
+ uploadFromRequest(req: Request, options: SomewhereFsUploadFromRequestOptions): Promise<SomewhereFsUploadFromRequestResult>;
1458
+ }
1459
+ interface SomewhereRuntimeFs extends SomewhereFsView { readonly server: SomewhereFsView }
1460
+
1461
+ // ---- sw.search
1462
+ interface SomewhereSearchManagedAddResult { path: string; version: number; chunks: number; searchable: true }
1463
+ interface SomewhereSearchManagedHit {
1464
+ content: string;
1465
+ score: number;
1466
+ source: { path: string; page: number | null; locator: string; url: string };
1467
+ }
1468
+ interface SomewhereSearchManagedQueryResult { query: string; results: SomewhereSearchManagedHit[] }
1469
+ interface SomewhereSearchIndexInfo { name: string; item_count: number; created_at: string }
1470
+ interface SomewhereSearchItem { id: string; content: string; metadata?: Readonly<Record<string, unknown>> | null }
1471
+ interface SomewhereSearchUpsertOptions { index: string; items: readonly SomewhereSearchItem[] }
1472
+ interface SomewhereSearchUpsertResult { index: string; upserted: number; item_count: number }
1473
+ type SomewhereSearchMode = 'hybrid' | 'semantic' | 'lexical';
1474
+ interface SomewhereSearchQueryOptions {
1475
+ index: string;
1476
+ query: string;
1477
+ limit?: number;
1478
+ offset?: number;
1479
+ mode?: SomewhereSearchMode;
1480
+ }
1481
+ interface SomewhereSearchHit {
1482
+ id: string;
1483
+ score: number;
1484
+ content: string;
1485
+ metadata: SomewhereJsonObject | null;
1486
+ // snippet/signals are present in hybrid and lexical mode.
1487
+ snippet?: string;
1488
+ signals?: { semantic?: number; lexical?: number };
1489
+ }
1490
+ interface SomewhereSearchQueryResult {
1491
+ results: SomewhereSearchHit[];
1492
+ page: { offset: number; limit: number; total: number; has_more: boolean };
1493
+ mode: SomewhereSearchMode;
1494
+ }
1495
+ interface SomewhereSearchRemoveOptions { index: string; ids: readonly string[] }
1496
+ interface SomewhereSearchRemoveResult { index: string; removed: number; item_count: number }
1497
+ interface SomewhereRuntimeSearch {
1498
+ // Managed file search: indexes a file owned by the signed-in user.
1499
+ add(path: string): Promise<SomewhereSearchManagedAddResult>;
1500
+ createIndex(name: string): Promise<SomewhereSearchIndexInfo>;
1501
+ listIndexes(): Promise<{ indexes: SomewhereSearchIndexInfo[] }>;
1502
+ deleteIndex(name: string): Promise<{ name: string; deleted: true }>;
1503
+ upsert(options: SomewhereSearchUpsertOptions): Promise<SomewhereSearchUpsertResult>;
1504
+ // String form searches the signed-in user's managed files; object form queries a named index.
1505
+ query(query: string): Promise<SomewhereSearchManagedQueryResult>;
1506
+ query(options: SomewhereSearchQueryOptions): Promise<SomewhereSearchQueryResult>;
1507
+ remove(options: SomewhereSearchRemoveOptions): Promise<SomewhereSearchRemoveResult>;
1508
+ }
1509
+
1510
+ // ---- sw.image
1511
+ interface SomewhereImageResizeOptions {
1512
+ width?: number;
1513
+ height?: number;
1514
+ fit?: 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad';
1515
+ format?: 'auto' | 'webp' | 'avif' | 'jpeg' | 'png' | 'json' | 'baseline-jpeg';
1516
+ quality?: number;
1517
+ dpr?: number;
1518
+ gravity?: string;
1519
+ background?: string;
1520
+ blur?: number;
1521
+ sharpen?: number;
1522
+ rotate?: 0 | 90 | 180 | 270;
1523
+ trim?: string;
1524
+ metadata?: 'keep' | 'copyright' | 'none';
1525
+ anim?: boolean;
1526
+ brightness?: number;
1527
+ contrast?: number;
1528
+ gamma?: number;
1529
+ border?: string;
1530
+ }
1531
+ interface SomewhereRuntimeImage {
1532
+ // Synchronous: returns the transform URL string; throws on an empty source.
1533
+ resize(source: string, options?: SomewhereImageResizeOptions | null): string;
1534
+ }
1535
+
1536
+ // ---- sw.render
1537
+ type __SomewhereRenderTarget = { url: string; html?: string } | { url?: string; html: string };
1538
+ type SomewhereRenderScreenshotOptions = __SomewhereRenderTarget & {
1539
+ width?: number;
1540
+ height?: number;
1541
+ format?: 'png' | 'jpeg' | 'webp';
1542
+ quality?: number;
1543
+ full_page?: boolean;
1544
+ // CSS selector to wait for before capture.
1545
+ wait_for?: string;
1546
+ local_storage?: Readonly<Record<string, string>>;
1547
+ cookies?: readonly { name: string; value: string }[];
1548
+ headers?: Readonly<Record<string, string>>;
1549
+ };
1550
+ type SomewhereRenderPdfOptions = __SomewhereRenderTarget & {
1551
+ format?: 'A4' | 'A3' | 'Letter' | 'Legal' | 'Tabloid';
1552
+ landscape?: boolean;
1553
+ print_background?: boolean;
1554
+ wait_for?: string;
1555
+ };
1556
+ interface SomewhereRenderStoredResult { storage_path: string; size_bytes: number; content_type: string }
1557
+ interface SomewhereRuntimeRender {
1558
+ // With storage: writes to project files and returns its path; without: the raw image Response.
1559
+ screenshot(options: SomewhereRenderScreenshotOptions & { storage: string }): Promise<SomewhereRenderStoredResult>;
1560
+ screenshot(options: SomewhereRenderScreenshotOptions & { storage?: null }): Promise<Response>;
1561
+ screenshot(options: SomewhereRenderScreenshotOptions & { storage?: string | null }): Promise<Response | SomewhereRenderStoredResult>;
1562
+ pdf(options: SomewhereRenderPdfOptions & { storage: string }): Promise<SomewhereRenderStoredResult>;
1563
+ pdf(options: SomewhereRenderPdfOptions & { storage?: null }): Promise<Response>;
1564
+ pdf(options: SomewhereRenderPdfOptions & { storage?: string | null }): Promise<Response | SomewhereRenderStoredResult>;
1565
+ }
1566
+
1567
+ // ---- sw.web
1568
+ type SomewhereWebScrapeFormat = 'markdown' | 'html' | 'rawHtml' | 'links' | 'screenshot';
1569
+ interface SomewhereWebScrapeOptions {
1570
+ formats?: readonly SomewhereWebScrapeFormat[];
1571
+ only_main?: boolean;
1572
+ // Milliseconds to let the page settle (max 30000).
1573
+ wait_for?: number;
1574
+ }
1575
+ interface SomewhereWebScrapeResult {
1576
+ url: string;
1577
+ title: string;
1578
+ description: string;
1579
+ language: string;
1580
+ status_code: number;
1581
+ markdown?: string;
1582
+ html?: string;
1583
+ raw_html?: string;
1584
+ links?: string[];
1585
+ // Inline data: URL when small, otherwise a short-lived scratch link.
1586
+ screenshot?: string | { scratch_url: string; expires_at: string };
1587
+ challenge_detected?: true;
1588
+ }
1589
+ interface SomewhereWebSearchOptions {
1590
+ count?: number;
1591
+ country?: string;
1592
+ freshness?: 'pd' | 'pw' | 'pm' | 'py';
1593
+ safesearch?: 'off' | 'moderate' | 'strict';
1594
+ }
1595
+ interface SomewhereWebSearchHit { url: string; title: string; description: string; age: string; source: string }
1596
+ interface SomewhereWebSearchResult { query: string; results: SomewhereWebSearchHit[]; count: number }
1597
+ interface SomewhereRuntimeWeb {
1598
+ scrape(url: string, options?: SomewhereWebScrapeOptions | null): Promise<SomewhereWebScrapeResult>;
1599
+ search(query: string, options?: SomewhereWebSearchOptions | null): Promise<SomewhereWebSearchResult>;
1600
+ }
1601
+
1602
+ // ---- sw.rateLimit
1603
+ interface SomewhereRateLimitResult {
1604
+ allowed: boolean;
1605
+ remaining: number;
1606
+ // Epoch seconds when the window rolls over.
1607
+ reset: number;
1608
+ limit: number;
1609
+ window_seconds: number;
1610
+ // false only in a preview check, where the limit is not counted.
1611
+ evaluated?: false;
1612
+ retry_after?: number;
1613
+ error?: string;
1614
+ message?: string;
1615
+ }
1616
+ interface SomewhereRuntimeRateLimit {
1617
+ check(key: string, max: number, windowSeconds: number): Promise<SomewhereRateLimitResult>;
1618
+ }
1619
+
1620
+ // ---- sw.logs
1621
+ type SomewhereLogLevel = 'debug' | 'info' | 'warn' | 'error';
1622
+ type SomewhereLogSource = 'server' | 'client' | 'job' | 'cron' | 'queue' | 'system' | 'function' | 'oauth';
1623
+ interface SomewhereLogsTailOptions {
1624
+ limit?: number;
1625
+ level?: SomewhereLogLevel;
1626
+ source?: SomewhereLogSource;
1627
+ search?: string;
1628
+ trace_id?: string;
1629
+ // ISO timestamp or epoch milliseconds.
1630
+ since?: string | number;
1631
+ }
1632
+ interface SomewhereLogEntry {
1633
+ id: string;
1634
+ level: SomewhereLogLevel;
1635
+ message: string;
1636
+ data: unknown;
1637
+ source: string | null;
1638
+ created_at: string;
1639
+ trace_id: string | null;
1640
+ }
1641
+ interface SomewhereTraceSpan {
1642
+ span_id: string;
1643
+ parent_span_id: string | null;
1644
+ name: string;
1645
+ kind: string;
1646
+ started_at: number;
1647
+ offset_ms: number;
1648
+ duration_ms: number;
1649
+ status: string;
1650
+ error_code: string | null;
1651
+ attributes: SomewhereJsonObject | null;
1652
+ depth: number;
1653
+ }
1654
+ interface SomewhereTraceNode extends SomewhereTraceSpan { children: SomewhereTraceNode[] }
1655
+ type SomewhereTraceResult =
1656
+ | { trace_id: string; found: false; reason: string; spans: []; waterfall: [] }
1657
+ | {
1658
+ trace_id: string;
1659
+ found: true;
1660
+ project_id: string | null;
1661
+ started_at: string;
1662
+ total_duration_ms: number;
1663
+ operation_count: number;
1664
+ truncated: boolean;
1665
+ slowest_operation: { name: string; duration_ms: number; span_id: string };
1666
+ failed_operations: { name: string; span_id: string; error_code: string | null }[];
1667
+ tree: SomewhereTraceNode[];
1668
+ waterfall: SomewhereTraceSpan[];
1669
+ evidence: {
1670
+ logs: SomewhereJsonObject[];
1671
+ errors: SomewhereJsonObject[];
1672
+ deploy_failures: SomewhereJsonObject[];
1673
+ journey_events: SomewhereJsonObject[];
1674
+ };
1675
+ };
1676
+ interface SomewhereRuntimeLogs {
1677
+ // Each write resolves with the platform's raw acceptance Response; throws when refused.
1678
+ debug(message: string, data?: unknown): Promise<Response>;
1679
+ info(message: string, data?: unknown): Promise<Response>;
1680
+ warn(message: string, data?: unknown): Promise<Response>;
1681
+ error(message: string, data?: unknown): Promise<Response>;
1682
+ tail(options?: SomewhereLogsTailOptions | null): Promise<SomewhereLogEntry[]>;
1683
+ // Omit the id to read the current request's trace.
1684
+ trace(traceId?: string | null): Promise<SomewhereTraceResult>;
1685
+ }
1686
+
1687
+ // ---- sw.analytics
1688
+ interface SomewhereAnalyticsTrackOptions {
1689
+ properties?: Readonly<Record<string, unknown>> | null;
1690
+ page?: string | null;
1691
+ referrer?: string | null;
1692
+ user_agent?: string | null;
1693
+ }
1694
+ interface SomewhereAnalyticsTrackResult {
1695
+ recorded: true;
1696
+ event: string;
1697
+ // Derived from the signed-in request user; never caller-supplied.
1698
+ user_id: string | null;
1699
+ attribution: 'app_user' | 'project';
1700
+ }
1701
+ interface SomewhereAnalyticsQueryOptions {
1702
+ event?: string;
1703
+ from?: string | number;
1704
+ to?: string | number;
1705
+ group_by?: 'hour' | 'day' | 'event' | 'user';
1706
+ limit?: number;
1707
+ }
1708
+ interface SomewhereAnalyticsQueryResult {
1709
+ // Row columns depend on group_by (bucket/event/user_id + count, or raw events).
1710
+ rows: SomewhereJsonObject[];
1711
+ count: number;
1712
+ group_by: 'hour' | 'day' | 'event' | 'user' | null;
1713
+ }
1714
+ interface SomewhereRuntimeAnalytics {
1715
+ track(event: string, options?: SomewhereAnalyticsTrackOptions | null): Promise<SomewhereAnalyticsTrackResult>;
1716
+ query(options?: SomewhereAnalyticsQueryOptions | null): Promise<SomewhereAnalyticsQueryResult>;
1717
+ }
1718
+
1719
+ interface SomewhereRuntimeContext {
1720
+ readonly fs: SomewhereRuntimeFs;
1721
+ readonly search: SomewhereRuntimeSearch;
1722
+ readonly image: SomewhereRuntimeImage;
1723
+ readonly render: SomewhereRuntimeRender;
1724
+ readonly web: SomewhereRuntimeWeb;
1725
+ /** @deprecated The global fetch is the same policy-checked outbound fetch. */
1726
+ readonly fetch: typeof fetch;
1727
+ readonly rateLimit: SomewhereRuntimeRateLimit;
1728
+ readonly logs: SomewhereRuntimeLogs;
1729
+ readonly analytics: SomewhereRuntimeAnalytics;
1730
+ }
1731
+ // ---- sw.ai ----
1732
+ type SomewhereAiProvider = 'anthropic' | 'openai' | 'xai' | 'workers-ai' | 'deepseek' | 'deepinfra';
1733
+ interface SomewhereAiInputBlock { type: string; [key: string]: unknown }
1734
+ interface SomewhereAiMessage {
1735
+ role: 'user' | 'assistant' | 'system';
1736
+ content: string | readonly SomewhereAiInputBlock[];
1737
+ }
1738
+ // Normalized content block: text, tool_use (id/name/input) or a provider-native block.
1739
+ interface SomewhereAiContentBlock {
1740
+ type: string;
1741
+ text?: string;
1742
+ id?: string;
1743
+ name?: string;
1744
+ input?: SomewhereJsonObject;
1745
+ [key: string]: unknown;
1746
+ }
1747
+ interface SomewhereAiToolDefinition {
1748
+ name: string;
1749
+ description?: string;
1750
+ input_schema: SomewhereJsonObject;
1751
+ type?: 'custom';
1752
+ cache_control?: SomewhereJsonObject;
1753
+ }
1754
+ type SomewhereAiSettledCost = { total_cents: number; total: string; status?: never };
1755
+ type SomewhereAiCost = SomewhereAiSettledCost | { status: 'pending'; total_cents: null; total: null };
1756
+ type SomewhereAiCompaction = 'truncate' | 'summarize'
1757
+ | { mode: 'truncate' | 'summarize'; provider?: SomewhereAiProvider; model?: string };
1758
+ interface SomewhereAiChatOptions {
1759
+ messages: readonly SomewhereAiMessage[];
1760
+ provider?: SomewhereAiProvider;
1761
+ model?: string;
1762
+ system?: string;
1763
+ max_tokens?: number;
1764
+ tools?: readonly SomewhereAiToolDefinition[];
1765
+ tool_choice?: string | SomewhereJsonObject;
1766
+ response_schema?: SomewhereJsonObject;
1767
+ // true returns the raw SSE body (anthropic only; not with conversation_id/response_schema).
1768
+ stream?: boolean;
1769
+ conversation_id?: string;
1770
+ compaction?: SomewhereAiCompaction;
1771
+ history_max_messages?: number;
1772
+ history_max_tokens?: number;
1773
+ idempotency_key?: string;
1774
+ service_tier?: 'standard' | 'flex';
1775
+ }
1776
+ interface SomewhereAiChatResult {
1777
+ content: SomewhereAiContentBlock[];
1778
+ text: string;
1779
+ stop_reason: string | null;
1780
+ model: string | null;
1781
+ provider: string;
1782
+ usage: { input_tokens: number | null; output_tokens: number | null };
1783
+ parsed?: SomewhereJsonObject | null;
1784
+ parse_error?: string | null;
1785
+ fallback_used?: boolean;
1786
+ fallback_provider?: string | null;
1787
+ service_tier?: 'standard' | 'flex' | null;
1788
+ // Non-enumerable: readable directly, dropped by spread / JSON.stringify.
1789
+ cost: SomewhereAiCost;
1790
+ conversation_id?: string;
1791
+ conversation_subject_type?: string | null;
1792
+ conversation_subject_id?: string | null;
1793
+ conversation_truncated?: boolean;
1794
+ conversation_summarized?: boolean;
1795
+ conversation_turn_id?: string;
1796
+ conversation_turn_state?: string;
1797
+ }
1798
+ interface __SomewhereAiChatFn {
1799
+ (options: SomewhereAiChatOptions & { stream: true }): Promise<ReadableStream<Uint8Array>>;
1800
+ (options: SomewhereAiChatOptions & { stream?: false }): Promise<SomewhereAiChatResult>;
1801
+ (options: SomewhereAiChatOptions): Promise<SomewhereAiChatResult | ReadableStream<Uint8Array>>;
1802
+ }
1803
+ interface SomewhereAiConversationSummary {
1804
+ id: string;
1805
+ client_conversation_id: string | null;
1806
+ subject_type: string;
1807
+ subject_id: string;
1808
+ created_at: string;
1809
+ updated_at: string;
1810
+ summary_updated_at: string | null;
1811
+ message_count: number;
1812
+ summarized_message_count: number;
1813
+ }
1814
+ interface SomewhereAiConversationMessage {
1815
+ id: number;
1816
+ role: string;
1817
+ content: string | SomewhereAiContentBlock[];
1818
+ token_count: number | null;
1819
+ summarized_at: string | null;
1820
+ created_at: string;
1821
+ }
1822
+ interface SomewhereAiConversationTurn {
1823
+ id: string;
1824
+ state: string;
1825
+ stored_state: string;
1826
+ provider: string | null;
1827
+ model: string | null;
1828
+ usage: unknown;
1829
+ error: string | null;
1830
+ created_at: string;
1831
+ updated_at: string;
1832
+ }
1833
+ interface SomewhereAiConversation {
1834
+ id: string;
1835
+ client_conversation_id: string | null;
1836
+ subject_type: string;
1837
+ subject_id: string;
1838
+ summary: string | null;
1839
+ summary_updated_at: string | null;
1840
+ created_at: string;
1841
+ updated_at: string;
1842
+ messages: SomewhereAiConversationMessage[];
1843
+ turns: SomewhereAiConversationTurn[];
1844
+ }
1845
+ interface SomewhereAiConversationFork {
1846
+ forked: true;
1847
+ source_conversation_id: string;
1848
+ new_conversation_id: string;
1849
+ message_count: number;
1850
+ subject_type: string;
1851
+ subject_id: string;
1852
+ }
1853
+ interface SomewhereAiConversationsAccessor {
1854
+ list(options?: { limit?: number }): Promise<{ conversations: SomewhereAiConversationSummary[] }>;
1855
+ // include: 'summarized' also returns messages already folded into the summary.
1856
+ get(id: string, options?: { include?: string }): Promise<SomewhereAiConversation>;
1857
+ delete(id: string): Promise<{ deleted: boolean; conversation_id: string }>;
1858
+ fork(sourceId: string, newId: string, options?: { upToMessageId?: number }): Promise<SomewhereAiConversationFork>;
1859
+ }
1860
+ interface SomewhereAiConversations extends SomewhereAiConversationsAccessor {
1861
+ // Developer capability: another end-user's conversations, without a signed-in request.
1862
+ forUser(userId: string): SomewhereAiConversationsAccessor;
1863
+ }
1864
+ interface SomewhereAiScoped {
1865
+ readonly chat: __SomewhereAiChatFn;
1866
+ readonly complete: __SomewhereAiChatFn;
1867
+ chatWithTools(options: SomewhereAiChatWithToolsOptions): Promise<SomewhereAgentRunResult>;
1868
+ readonly conversations: SomewhereAiConversationsAccessor;
1869
+ }
1870
+ interface SomewhereAiUserMemoryCompactOptions {
1871
+ conversation_id?: string;
1872
+ history?: string;
1873
+ windowMessages?: number;
1874
+ provider?: SomewhereAiProvider;
1875
+ model?: string;
1876
+ maxTokens?: number;
1877
+ }
1878
+ // Subject is the request's verified user; every call requires a signed-in user.
1879
+ interface SomewhereAiUserMemory {
1880
+ get(): Promise<SomewhereJsonObject>;
1881
+ update(patch: SomewhereJsonObject): Promise<SomewhereJsonObject>;
1882
+ clear(): Promise<{ cleared: true }>;
1883
+ compact(schema: SomewhereJsonObject, options?: SomewhereAiUserMemoryCompactOptions): Promise<SomewhereJsonObject>;
1884
+ }
1885
+ type SomewhereAiTranscribeOptions = { model?: string } & (
1886
+ | { audio: string; audio_url?: string }
1887
+ | { audio?: string; audio_url: string }
1888
+ );
1889
+ interface SomewhereAiTranscribeResult {
1890
+ text: string;
1891
+ duration_seconds: number;
1892
+ words: { word: string; start: number; end: number }[];
1893
+ model: string;
1894
+ cost: SomewhereAiSettledCost;
1895
+ }
1896
+ interface __SomewhereAiStoredFile {
1897
+ storage_path: string;
1898
+ size_bytes: number;
1899
+ content_type: string;
1900
+ model: string;
1901
+ owner_subject_type: 'app_user' | 'project_owner';
1902
+ owner_subject_id: string;
1903
+ cost: SomewhereAiSettledCost;
1904
+ }
1905
+ interface SomewhereAiTtsOptions {
1906
+ text: string;
1907
+ model?: string;
1908
+ voice?: string;
1909
+ lang?: string;
1910
+ // Set: the audio is saved to this file path and a JSON envelope returns; unset: raw audio Response.
1911
+ storage?: string;
1912
+ }
1913
+ interface SomewhereAiTtsStored extends __SomewhereAiStoredFile { duration_seconds_estimate: number }
1914
+ interface SomewhereAiGenerateImageOptions {
1915
+ prompt: string;
1916
+ model?: string;
1917
+ // Accepted and ignored: the model selects the provider.
1918
+ provider?: SomewhereAiProvider;
1919
+ width?: number;
1920
+ height?: number;
1921
+ steps?: number;
1922
+ storage?: string;
1923
+ }
1924
+ interface SomewhereAiGenerateImageStored extends __SomewhereAiStoredFile { width: number; height: number; steps: number }
1925
+ interface SomewhereAiRemoveBackgroundOptions { image_url: string; model?: string; storage?: string }
1926
+ type SomewhereAiEmbeddingsOptions = {
1927
+ model?: string;
1928
+ dimensions?: number;
1929
+ // Accepted and ignored: the model selects the provider.
1930
+ provider?: SomewhereAiProvider;
1931
+ } & (
1932
+ | { text: string | readonly string[]; input?: never }
1933
+ | { input: string | readonly string[]; text?: never }
1934
+ );
1935
+ interface SomewhereAiEmbeddingsResult {
1936
+ model: string;
1937
+ provider: string;
1938
+ dimensions: number;
1939
+ count: number;
1940
+ embeddings: number[][];
1941
+ usage: { input_tokens: number };
1942
+ cost: SomewhereAiSettledCost;
1943
+ }
1944
+ interface SomewhereAiModerationResult {
1945
+ flagged: boolean;
1946
+ categories: string[];
1947
+ scores: Record<string, number>;
1948
+ model: string;
1949
+ provider: string;
1950
+ }
1951
+ interface SomewhereAiCatalogEntry {
1952
+ feature: 'transcribe' | 'tts' | 'generate_image' | 'remove_background' | 'embed' | 'complete';
1953
+ model: string;
1954
+ provider: string;
1955
+ label: string;
1956
+ pricing: string;
1957
+ at_cost: boolean;
1958
+ free: boolean;
1959
+ note?: string;
1960
+ alias_for?: string;
1961
+ voices?: Record<string, readonly string[]>;
1962
+ }
1963
+ interface SomewhereAiCatalog {
1964
+ your_tier: string;
1965
+ rate_limits: SomewhereJsonObject;
1966
+ rate_limits_by_tier: SomewhereJsonObject;
1967
+ free_complete_default: SomewhereAiCatalogEntry & { limits: SomewhereJsonObject };
1968
+ models: SomewhereAiCatalogEntry[];
1969
+ }
1970
+ interface SomewhereRuntimeAi {
1971
+ readonly chat: __SomewhereAiChatFn;
1972
+ readonly complete: __SomewhereAiChatFn;
1973
+ // Runs the sw.agent loop; executeTools is required and maxIterations defaults to 5.
1974
+ chatWithTools(options: SomewhereAiChatWithToolsOptions): Promise<SomewhereAgentRunResult>;
1975
+ readonly conversations: SomewhereAiConversations;
1976
+ readonly userMemory: SomewhereAiUserMemory;
1977
+ scoped(subjectId: string, subjectType?: string): SomewhereAiScoped;
1978
+ forUser(userId: string): SomewhereAiScoped;
1979
+ transcribe(options: SomewhereAiTranscribeOptions): Promise<SomewhereAiTranscribeResult>;
1980
+ tts(options: SomewhereAiTtsOptions & { storage: string }): Promise<SomewhereAiTtsStored>;
1981
+ tts(options: SomewhereAiTtsOptions & { storage?: undefined }): Promise<Response>;
1982
+ tts(options: SomewhereAiTtsOptions): Promise<SomewhereAiTtsStored | Response>;
1983
+ generateImage(options: SomewhereAiGenerateImageOptions & { storage: string }): Promise<SomewhereAiGenerateImageStored>;
1984
+ generateImage(options: SomewhereAiGenerateImageOptions & { storage?: undefined }): Promise<Response>;
1985
+ generateImage(options: SomewhereAiGenerateImageOptions): Promise<SomewhereAiGenerateImageStored | Response>;
1986
+ // Managed background removal is retired: every call rejects with VALIDATION_ERROR.
1987
+ removeBackground(options: SomewhereAiRemoveBackgroundOptions): Promise<never>;
1988
+ embeddings(options: SomewhereAiEmbeddingsOptions): Promise<SomewhereAiEmbeddingsResult>;
1989
+ moderate(text: string): Promise<SomewhereAiModerationResult>;
1990
+ catalog(): Promise<SomewhereAiCatalog>;
1991
+ }
1992
+
1993
+ // ---- sw.agent ----
1994
+ interface SomewhereAgentToolContext { agentId: string | null; turn: number; toolCallId: string }
1995
+ type SomewhereAgentToolExecute = (input: SomewhereJsonObject, context: SomewhereAgentToolContext) => unknown;
1996
+ interface SomewhereAgentKeyedTool {
1997
+ description?: string;
1998
+ inputSchema?: SomewhereJsonObject;
1999
+ input_schema?: SomewhereJsonObject;
2000
+ // Required unless options.executeTools is supplied.
2001
+ execute?: SomewhereAgentToolExecute;
2002
+ }
2003
+ interface SomewhereAgentTool extends SomewhereAgentKeyedTool { name: string }
2004
+ interface SomewhereAgentToolCall { id: string; name: string; input: SomewhereJsonObject }
2005
+ interface SomewhereAgentToolResultRow { tool_use_id: string; content: unknown; is_error?: boolean }
2006
+ interface SomewhereAgentToolResultBlock { type: 'tool_result'; tool_use_id: string; content: string; is_error?: true }
2007
+ interface SomewhereAgentStep {
2008
+ step_number: number;
2009
+ turn: number;
2010
+ provider: string | null;
2011
+ model: string | null;
2012
+ content: SomewhereAiContentBlock[];
2013
+ text: string;
2014
+ output: string | null;
2015
+ stop_reason: string | null;
2016
+ tool_calls: { id: string; name: string; input: SomewhereJsonObject; is_error: boolean }[];
2017
+ tool_results: SomewhereAgentToolResultBlock[];
2018
+ usage: { input_tokens: number | null; output_tokens: number | null } | null;
2019
+ started_at: number;
2020
+ duration_ms: number;
2021
+ completion_reason: string;
2022
+ on_step?: unknown;
2023
+ // Non-enumerable; null while the step's cost is pending.
2024
+ readonly cost_cents: number | null;
2025
+ }
2026
+ interface SomewhereAgentPrepareStepContext {
2027
+ stepNumber: number;
2028
+ steps: SomewhereAgentStep[];
2029
+ messages: SomewhereAiMessage[];
2030
+ provider: SomewhereAiProvider | undefined;
2031
+ model: string | undefined;
2032
+ system: string | undefined;
2033
+ tools: SomewhereAiToolDefinition[] | undefined;
2034
+ toolChoice: string | SomewhereJsonObject | undefined;
2035
+ maxTokens: number | undefined;
2036
+ }
2037
+ interface SomewhereAgentPrepareStepOverride {
2038
+ messages?: readonly SomewhereAiMessage[];
2039
+ provider?: SomewhereAiProvider;
2040
+ model?: string;
2041
+ system?: string;
2042
+ tools?: readonly SomewhereAiToolDefinition[];
2043
+ toolChoice?: string | SomewhereJsonObject;
2044
+ maxTokens?: number;
2045
+ }
2046
+ interface SomewhereAgentStepEvent { step: SomewhereAgentStep; steps: SomewhereAgentStep[] }
2047
+ interface SomewhereAgentOnStepEvent {
2048
+ agentId: string | null;
2049
+ turn: number;
2050
+ maxTurns: number;
2051
+ output: string | null;
2052
+ toolCalls: { id: string; name: string; is_error: boolean }[];
2053
+ done: boolean;
2054
+ stopReason: string | null;
2055
+ }
2056
+ type __SomewhereMaybePromise<T> = T | Promise<T>;
2057
+ interface SomewhereAgentOptions extends Omit<SomewhereAiChatOptions, 'messages' | 'tools' | 'stream'> {
2058
+ // One input source is required: a non-empty messages array, prompt, or input.
2059
+ messages?: readonly SomewhereAiMessage[];
2060
+ prompt?: string;
2061
+ input?: string;
2062
+ systemPrompt?: string;
2063
+ serviceTier?: 'standard' | 'flex';
2064
+ maxTokens?: number;
2065
+ tools?: readonly SomewhereAgentTool[] | Readonly<Record<string, SomewhereAgentKeyedTool>>;
2066
+ executeTools?: (calls: SomewhereAgentToolCall[]) => __SomewhereMaybePromise<readonly SomewhereAgentToolResultRow[]>;
2067
+ // Integer 1-20; first defined of maxSteps, maxIterations, maxTurns wins (default 8).
2068
+ maxSteps?: number;
2069
+ maxIterations?: number;
2070
+ maxTurns?: number;
2071
+ maxSpendCents?: number;
2072
+ prepareStep?: (context: SomewhereAgentPrepareStepContext) => __SomewhereMaybePromise<SomewhereAgentPrepareStepOverride | null | undefined | void>;
2073
+ onStepFinish?: (event: SomewhereAgentStepEvent) => unknown;
2074
+ stopWhen?: (event: SomewhereAgentStepEvent) => __SomewhereMaybePromise<boolean | string | null | undefined>;
2075
+ onStep?: (event: SomewhereAgentOnStepEvent) => unknown;
2076
+ }
2077
+ interface SomewhereAiChatWithToolsOptions extends SomewhereAgentOptions {
2078
+ executeTools: (calls: SomewhereAgentToolCall[]) => __SomewhereMaybePromise<readonly SomewhereAgentToolResultRow[]>;
2079
+ }
2080
+ interface SomewhereAgentDurableOptions extends SomewhereAgentOptions { model: string }
2081
+ interface SomewhereAgentRunResult extends Omit<SomewhereAiChatResult, 'cost'> {
2082
+ iterations: number;
2083
+ tool_calls_made: number;
2084
+ total_input_tokens: number;
2085
+ total_output_tokens: number;
2086
+ completion_reason: string;
2087
+ // Non-enumerable: readable directly, dropped by spread / JSON.stringify.
2088
+ readonly steps: SomewhereAgentStep[];
2089
+ readonly total_cost_cents: number | null;
2090
+ }
2091
+ // Returned instead when this invocation is the platform's signed step callback; return it from the handler.
2092
+ interface SomewhereAgentStepCheckpoint {
2093
+ __sw_agent_turn: true;
2094
+ state: SomewhereJsonObject;
2095
+ done: boolean;
2096
+ agent_id?: never;
2097
+ status?: never;
2098
+ max_steps?: never;
2099
+ max_turns?: never;
2100
+ }
2101
+ type SomewhereAgentStartResult = SomewhereAgentStepCheckpoint
2102
+ | { agent_id: string; status: string; max_steps: number; __sw_agent_turn?: never; state?: never; done?: never };
2103
+ type SomewhereAgentLegacyStartResult = SomewhereAgentStepCheckpoint
2104
+ | { agent_id: string; status: string; max_turns: number; __sw_agent_turn?: never; state?: never; done?: never };
2105
+ interface SomewhereAgentStatus {
2106
+ agent_id: string;
2107
+ job_id: string;
2108
+ project_id: string;
2109
+ handler: string;
2110
+ status: string;
2111
+ progress: number;
2112
+ progress_message: string | null;
2113
+ payload: unknown;
2114
+ result: unknown;
2115
+ error: string | null;
2116
+ error_code: string | null;
2117
+ webhook_url: string | null;
2118
+ webhook_delivered: boolean;
2119
+ timeout_seconds: number;
2120
+ attempts: unknown[];
2121
+ priority: string;
2122
+ created_at: string;
2123
+ started_at: string | null;
2124
+ completed_at: string | null;
2125
+ last_heartbeat_at: string | null;
2126
+ ownership_status: 'app_user' | 'project_owner';
2127
+ owner_subject_id: string | null;
2128
+ cron_id: string | null;
2129
+ cron_scheduled_at: string | null;
2130
+ trigger: string | null;
2131
+ recovery: { dispatch_state: string; workflow_state: string | null; recovery_error: string | null; observed_at: number | null } | null;
2132
+ }
2133
+ interface SomewhereAgentCancelResult {
2134
+ agent_id: string;
2135
+ job_id: string;
2136
+ status: 'cancelled';
2137
+ ownership_status: 'app_user' | 'project_owner';
2138
+ owner_subject_id: string | null;
2139
+ }
2140
+ interface SomewhereRuntimeAgent {
2141
+ // Durable compatibility form: same as start() but reports max_turns.
2142
+ (options: SomewhereAgentDurableOptions): Promise<SomewhereAgentLegacyStartResult>;
2143
+ run(options: SomewhereAgentOptions): Promise<SomewhereAgentRunResult>;
2144
+ start(options: SomewhereAgentDurableOptions): Promise<SomewhereAgentStartResult>;
2145
+ status(agentId: string): Promise<SomewhereAgentStatus>;
2146
+ cancel(agentId: string): Promise<SomewhereAgentCancelResult>;
2147
+ }
2148
+
2149
+ interface SomewhereRuntimeContext { readonly ai: SomewhereRuntimeAi; readonly agent: SomewhereRuntimeAgent }
2150
+ // \u2500\u2500 sw.email / sw.contacts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2151
+ type SomewhereEmailStatus =
2152
+ | 'claimed' | 'pending' | 'sent' | 'delivered' | 'opened' | 'clicked'
2153
+ | 'bounced' | 'complained' | 'suppressed' | 'preference_blocked'
2154
+ | 'delivery_unknown' | 'failed' | 'captured';
2155
+ type SomewhereEmailTopic = 'project-updates' | 'milestones' | 'announcements' | 'founder-notes';
2156
+ // At least one of html / text is required.
2157
+ type __SomewhereEmailBody = { html: string; text?: string } | { text: string; html?: string };
2158
+ type SomewhereEmailSendOptions = {
2159
+ // One recipient address.
2160
+ to: string;
2161
+ subject: string;
2162
+ // Omit for the platform-managed sender; otherwise a verified sender domain on this project.
2163
+ from?: string;
2164
+ reply_to?: string;
2165
+ // 'marketing' requires a topic.
2166
+ category?: 'transactional' | 'marketing';
2167
+ subtype?: string;
2168
+ topic?: SomewhereEmailTopic;
2169
+ template_key?: string;
2170
+ campaign_key?: string;
2171
+ journey_key?: string;
2172
+ step_key?: string;
2173
+ idempotency_key?: string;
2174
+ in_reply_to?: string;
2175
+ references?: readonly string[];
2176
+ app_user_id?: string;
2177
+ } & __SomewhereEmailBody;
2178
+ interface SomewhereEmailSendResult {
2179
+ id: string;
2180
+ message_id: string;
2181
+ // Absent on a captured (test inbox) send.
2182
+ tracking_id?: string;
2183
+ // A new send is 'sent' | 'pending' | 'captured'; a duplicate replays the stored state.
2184
+ status: SomewhereEmailStatus;
2185
+ duplicate?: true;
2186
+ sender?: { from: string; mode: 'platform_managed'; note: string };
2187
+ test_inbox?: { address: string; delivered: false };
2188
+ }
2189
+ interface SomewhereEmailEventCounts {
2190
+ sent: number; delivered: number; opened: number; clicked: number;
2191
+ bounced: number; complained: number; delivery_delayed: number; total: number;
2192
+ }
2193
+ interface SomewhereEmailHistoryMessage {
2194
+ id: string;
2195
+ tracking_id: string;
2196
+ provider_message_id: string;
2197
+ contact_id: string | null;
2198
+ category: 'transactional' | 'marketing';
2199
+ subtype: string | null;
2200
+ topic: string | null;
2201
+ template_key: string | null;
2202
+ campaign_key: string | null;
2203
+ journey_key: string | null;
2204
+ step_key: string | null;
2205
+ idempotency_key: string | null;
2206
+ message_id_header: string | null;
2207
+ from_address: string;
2208
+ to_address: string;
2209
+ subject: string;
2210
+ status: string;
2211
+ created_at: number;
2212
+ sent_at: number;
2213
+ latest_at: number;
2214
+ event_counts: SomewhereEmailEventCounts;
2215
+ env_slot: string;
2216
+ }
2217
+ interface SomewhereEmailStatusResult {
2218
+ // The stored message row (id, status, recipients, ...).
2219
+ message: SomewhereJsonObject;
2220
+ events: SomewhereJsonObject[];
2221
+ }
2222
+ interface SomewhereEmailSuppression {
2223
+ address: string;
2224
+ suppressed: boolean;
2225
+ status: 'bounced' | 'complained' | null;
2226
+ last_at: string | null;
2227
+ occurrences: number;
2228
+ }
2229
+ interface SomewhereEmailBounce {
2230
+ address: string;
2231
+ last_status: 'bounced' | 'complained';
2232
+ last_at: string;
2233
+ occurrences: number;
2234
+ }
2235
+ interface SomewhereEmailTestInboxMessage {
2236
+ id: string;
2237
+ to: string;
2238
+ subject: string;
2239
+ html: string | null;
2240
+ text: string | null;
2241
+ magic_link: string | null;
2242
+ created_at: string;
2243
+ }
2244
+ interface SomewhereRuntimeEmail {
2245
+ send(message: SomewhereEmailSendOptions): Promise<SomewhereEmailSendResult>;
2246
+ history(options?: { limit?: number; offset?: number } | null): Promise<{ messages: SomewhereEmailHistoryMessage[] }>;
2247
+ status(id: string): Promise<SomewhereEmailStatusResult>;
2248
+ checkSuppression(address: string): Promise<SomewhereEmailSuppression>;
2249
+ bounces(options?: { days?: number; limit?: number } | null): Promise<{ bounces: SomewhereEmailBounce[]; window_days: number; limit: number }>;
2250
+ // Reads only this project's <anything>@<subdomain>.test.somewhere.site test inbox.
2251
+ inbox(address: string, options?: { limit?: number } | null): Promise<{ address: string; messages: SomewhereEmailTestInboxMessage[]; limit: number }>;
2252
+ }
2253
+ interface SomewhereEmailContactProperties {
2254
+ email_topics?: Partial<Record<SomewhereEmailTopic, boolean>>;
2255
+ tags?: readonly string[];
2256
+ unsubscribed?: boolean;
2257
+ app_user_id?: string;
2258
+ source?: string;
2259
+ // Reserved: the platform binds this; sending it is a VALIDATION_ERROR.
2260
+ platform_user_id?: never;
2261
+ [key: string]: unknown;
2262
+ }
2263
+ interface SomewhereEmailContact {
2264
+ id: string;
2265
+ display_name: string | null;
2266
+ properties: SomewhereEmailContactProperties;
2267
+ email: string;
2268
+ normalized_email: string;
2269
+ is_primary: boolean;
2270
+ created_at: number;
2271
+ updated_at: number;
2272
+ }
2273
+ interface SomewhereRuntimeContacts {
2274
+ upsert(contact: { email: string; display_name?: string | null; properties?: SomewhereEmailContactProperties; id?: string }): Promise<{ contact: SomewhereEmailContact }>;
2275
+ get(idOrEmail: string): Promise<{ contact: SomewhereEmailContact }>;
2276
+ list(options?: { limit?: number; offset?: number } | null): Promise<{ contacts: SomewhereEmailContact[] }>;
2277
+ }
2278
+
2279
+ // \u2500\u2500 sw.inbox \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2280
+ type SomewhereInboxOwnershipStatus = 'app_user' | 'project_owner' | 'legacy_project';
2281
+ // At least one of body / text / html is required (body is an alias of text).
2282
+ type __SomewhereInboxBody =
2283
+ | { body: string; text?: string; html?: string }
2284
+ | { text: string; body?: string; html?: string }
2285
+ | { html: string; body?: string; text?: string };
2286
+ interface SomewhereInboxAddressCreateOptions {
2287
+ address: string;
2288
+ label?: string;
2289
+ webhook_url?: string;
2290
+ forward_to?: string;
2291
+ }
2292
+ interface SomewhereInboxAddressCreated {
2293
+ id: string;
2294
+ project_id: string;
2295
+ address: string;
2296
+ label: string | null;
2297
+ kind: 'admin' | 'app';
2298
+ ownership_status: 'app_user' | 'project_owner';
2299
+ webhook_url: string | null;
2300
+ // Returned once, only when webhook_url was set.
2301
+ webhook_secret: string | null;
2302
+ forward?: SomewhereJsonObject;
2303
+ created_at: string;
2304
+ }
2305
+ interface SomewhereInboxAddress {
2306
+ id: string;
2307
+ address: string;
2308
+ label: string | null;
2309
+ created_at: string;
2310
+ webhook_url: string | null;
2311
+ forward_to: string | null;
2312
+ forward_status: 'active' | 'pending_verification' | null;
2313
+ ownership_status: SomewhereInboxOwnershipStatus;
2314
+ }
2315
+ interface SomewhereInboxListOptions {
2316
+ address_id?: string;
2317
+ limit?: number;
2318
+ unread?: boolean;
2319
+ q?: string;
2320
+ include_spam?: boolean;
2321
+ }
2322
+ interface SomewhereInboxMessageSummary {
2323
+ id: string;
2324
+ address_id: string;
2325
+ mail_from: string;
2326
+ mail_to: string;
2327
+ subject: string | null;
2328
+ text_preview: string | null;
2329
+ has_html: boolean;
2330
+ attachment_count: number;
2331
+ size_bytes: number;
2332
+ read_at: string | null;
2333
+ received_at: string;
2334
+ thread_root: string | null;
2335
+ spf_result: string | null;
2336
+ dkim_result: string | null;
2337
+ dmarc_result: string | null;
2338
+ spam_suspect: boolean;
2339
+ ownership_status: SomewhereInboxOwnershipStatus;
2340
+ }
2341
+ interface SomewhereInboxAttachmentMeta {
2342
+ filename: string | null;
2343
+ content_type: string;
2344
+ size_bytes: number;
2345
+ r2_key: string;
2346
+ }
2347
+ interface SomewhereInboxMessage extends SomewhereInboxMessageSummary {
2348
+ project_id: string;
2349
+ r2_key: string;
2350
+ message_id_header: string | null;
2351
+ in_reply_to_header: string | null;
2352
+ references_header: string | null;
2353
+ attachments: SomewhereInboxAttachmentMeta[];
2354
+ raw_url: string;
2355
+ // Present only with include_html: true.
2356
+ html_preview?: string | null;
2357
+ }
2358
+ interface SomewhereInboxSendPending {
2359
+ id: string;
2360
+ message_id: string;
2361
+ status: 'pending';
2362
+ code: 'EMAIL_SEND_PENDING';
2363
+ duplicate?: true;
2364
+ }
2365
+ interface __SomewhereInboxSentBase {
2366
+ id: string;
2367
+ from: string;
2368
+ to: string;
2369
+ subject: string;
2370
+ thread_root: string;
2371
+ status?: never;
2372
+ duplicate?: true;
2373
+ test_inbox?: { address: string; delivered: false };
2374
+ }
2375
+ interface SomewhereInboxSent extends __SomewhereInboxSentBase { message_id: string }
2376
+ interface SomewhereInboxReplySent extends __SomewhereInboxSentBase { inbox_message_id: string }
2377
+ interface SomewhereInboxThreadSummary {
2378
+ thread_root: string;
2379
+ last_at: string;
2380
+ first_at: string;
2381
+ message_count: number;
2382
+ unread_count: number;
2383
+ last_subject: string | null;
2384
+ last_counterparty: string;
2385
+ last_direction: 'in' | 'out';
2386
+ ownership_status: SomewhereInboxOwnershipStatus;
2387
+ }
2388
+ type SomewhereInboxThreadMessage =
2389
+ | {
2390
+ direction: 'in'; id: string; address_id: string; mail_from: string; mail_to: string;
2391
+ subject: string | null; text_preview: string | null; has_html: boolean;
2392
+ attachment_count: number; read_at: string | null; received_at: string;
2393
+ spf_result: string | null; dkim_result: string | null; dmarc_result: string | null;
2394
+ spam_suspect: boolean; message_id: string | null; ownership_status: SomewhereInboxOwnershipStatus;
2395
+ }
2396
+ | {
2397
+ direction: 'out'; id: string; to: string; from: string; subject: string | null;
2398
+ body: string | null; text_preview: string | null; sent_at: string; is_reply: boolean;
2399
+ message_id: string | null; inbox_message_id: string | null; ownership_status: SomewhereInboxOwnershipStatus;
2400
+ };
2401
+ interface SomewhereInboxRule {
2402
+ id: string;
2403
+ address_id: string | null;
2404
+ pattern: string;
2405
+ action: 'allow' | 'deny';
2406
+ created_at: string;
2407
+ }
2408
+ interface SomewhereInboxRules {
2409
+ list(options?: { address_id?: string } | null): Promise<{ rules: SomewhereInboxRule[] }>;
2410
+ // Omit address_id for a project-wide rule (sw.inbox.project only).
2411
+ create(rule: { pattern: string; action: 'allow' | 'deny'; address_id?: string }): Promise<SomewhereInboxRule & { project_id: string }>;
2412
+ delete(id: string): Promise<{ id: string; deleted: true }>;
2413
+ }
2414
+ interface SomewhereInboxClient {
2415
+ listAddresses(): Promise<{ addresses: SomewhereInboxAddress[] }>;
2416
+ createAddress(options: SomewhereInboxAddressCreateOptions): Promise<SomewhereInboxAddressCreated>;
2417
+ listAppAddresses(): Promise<{ addresses: SomewhereInboxAddress[] }>;
2418
+ deleteAddress(id: string): Promise<{ id: string; deleted: true }>;
2419
+ list(options?: SomewhereInboxListOptions | null): Promise<{ messages: SomewhereInboxMessageSummary[]; count: number }>;
2420
+ get(id: string, options?: { include_html?: boolean } | null): Promise<SomewhereInboxMessage>;
2421
+ // Raw RFC-822 bytes; the Response carries X-Somewhere-Ownership-Status.
2422
+ raw(id: string): Promise<Response>;
2423
+ attachment(id: string, index: number): Promise<Response>;
2424
+ markRead(id: string, read?: boolean): Promise<{ id: string; read: boolean; ownership_status: SomewhereInboxOwnershipStatus; read_at: string | null }>;
2425
+ delete(id: string): Promise<{ id: string; deleted: true }>;
2426
+ reply(id: string, reply: { subject?: string; idempotency_key?: string } & __SomewhereInboxBody): Promise<SomewhereInboxReplySent | SomewhereInboxSendPending>;
2427
+ send(addressId: string, message: { to: string; subject: string; idempotency_key?: string } & __SomewhereInboxBody): Promise<SomewhereInboxSent | SomewhereInboxSendPending>;
2428
+ threads(options?: { address_id?: string; limit?: number; include_spam?: boolean } | null): Promise<{ threads: SomewhereInboxThreadSummary[] }>;
2429
+ thread(root: string): Promise<{ thread_root: string; messages: SomewhereInboxThreadMessage[] }>;
2430
+ readonly rules: SomewhereInboxRules;
2431
+ }
2432
+ interface SomewhereInboxGrant {
2433
+ collaborator_user_id: string;
2434
+ role: 'viewer' | 'editor';
2435
+ granted_by: string;
2436
+ created_at: string;
2437
+ }
2438
+ // Trusted project-scope inbox: createAddress makes admin mailboxes; owner-only grants/migration.
2439
+ interface SomewhereRuntimeProjectInbox extends SomewhereInboxClient {
2440
+ assignLegacyOwner(addressId: string, appUserId: string): Promise<{ id: string; ownership_status: 'app_user'; migrated_message_count: number; migration_id: string }>;
2441
+ readonly grants: {
2442
+ list(addressId: string): Promise<{ address_id: string; ownership_status: SomewhereInboxOwnershipStatus; grants: SomewhereInboxGrant[] }>;
2443
+ set(addressId: string, collaboratorUserId: string, role: 'viewer' | 'editor'): Promise<{ address_id: string; collaborator_user_id: string; role: 'viewer' | 'editor'; ownership_status: 'app_user'; created_at: string }>;
2444
+ delete(addressId: string, collaboratorUserId: string): Promise<{ address_id: string; collaborator_user_id: string; deleted: true }>;
2445
+ };
2446
+ }
2447
+ // Scoped to the request's verified app user; createAddress makes app mailboxes.
2448
+ interface SomewhereRuntimeInbox extends SomewhereInboxClient {
2449
+ readonly project: SomewhereRuntimeProjectInbox;
2450
+ }
2451
+
2452
+ // \u2500\u2500 sw.notifications \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2453
+ type SomewhereNotificationsChannel = 'bell' | 'push' | 'email';
2454
+ interface SomewhereNotificationsSendOptions {
2455
+ title?: string;
2456
+ body?: string;
2457
+ url?: string | null;
2458
+ // Recipient address; required for the email channel.
2459
+ email?: string;
2460
+ from?: string;
2461
+ // Default ['bell', 'push'].
2462
+ channels?: readonly SomewhereNotificationsChannel[];
2463
+ }
2464
+ interface SomewhereNotificationsChannelFailure { ok: false; error: string; code?: string }
2465
+ interface SomewhereNotificationsSendResult {
2466
+ bell?: { ok: true; id: string } | SomewhereNotificationsChannelFailure;
2467
+ push?: ({ ok: true } & SomewherePushSendResult) | SomewhereNotificationsChannelFailure;
2468
+ email?: ({ ok: true } & SomewhereEmailSendResult) | SomewhereNotificationsChannelFailure;
2469
+ }
2470
+ interface SomewhereNotification {
2471
+ id: string;
2472
+ title: string | null;
2473
+ body: string | null;
2474
+ url: string | null;
2475
+ // 0 or 1.
2476
+ read: number;
2477
+ created_at: number;
2478
+ }
2479
+ // list / unreadCount / markRead / markAllRead act on the request's verified user (AUTH_REQUIRED otherwise).
2480
+ interface SomewhereRuntimeNotifications {
2481
+ send(userId: string, options: SomewhereNotificationsSendOptions): Promise<SomewhereNotificationsSendResult>;
2482
+ list(options?: { limit?: number; unread_only?: boolean } | null): Promise<{ notifications: SomewhereNotification[]; count: number }>;
2483
+ unreadCount(): Promise<number>;
2484
+ markRead(notificationId: string): Promise<{ ok: true; changes: number } | { ok: false; error: string }>;
2485
+ markAllRead(): Promise<{ ok: true; changes: number }>;
2486
+ }
2487
+
2488
+ // \u2500\u2500 sw.push \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2489
+ interface SomewherePushSendOptions {
2490
+ // Required: a string or any JSON-serializable value.
2491
+ payload: string | number | boolean | object;
2492
+ // Neither user_id nor endpoint: broadcast to every subscription in the project.
2493
+ user_id?: string;
2494
+ userId?: string;
2495
+ endpoint?: string;
2496
+ ttl?: number;
2497
+ }
2498
+ interface SomewherePushSendResult { sent: number; failed: number; gone: number; recipients: number }
2499
+ // subscribe / unsubscribe are withdrawn in functions (always throw PUSH_SUBSCRIBE_UNAVAILABLE) and are not declared.
2500
+ interface SomewhereRuntimePush {
2501
+ vapidPublicKey(): Promise<{ vapid_public_key: string }>;
2502
+ send(options: SomewherePushSendOptions): Promise<SomewherePushSendResult>;
2503
+ }
2504
+
2505
+ // \u2500\u2500 sw.queue / sw.jobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2506
+ interface SomewhereQueuePushOptions {
2507
+ // A project-relative /api path or an https:// URL.
2508
+ handler: string;
2509
+ payload?: unknown;
2510
+ // Max 43200 (12 hours).
2511
+ delay_seconds?: number;
2512
+ idempotency_key?: string;
2513
+ queue_name?: string;
2514
+ }
2515
+ interface SomewhereRuntimeQueue {
2516
+ push(options: SomewhereQueuePushOptions): Promise<{ message_id: string; status: 'pending' | 'queued'; deduped?: true }>;
2517
+ // Same check as sw.jobs.verifyInvocation.
2518
+ verifyInvocation(req: Request): Promise<boolean>;
2519
+ }
2520
+ interface SomewhereJobsCreateOptions {
2521
+ handler: string;
2522
+ payload?: unknown;
2523
+ webhook_url?: string;
2524
+ timeout_seconds?: number;
2525
+ priority?: 'normal' | 'low';
2526
+ // Generated per call when omitted.
2527
+ idempotency_key?: string;
2528
+ agent?: { messages: readonly unknown[]; max_steps?: number; max_turns?: number; deployment_version?: string };
2529
+ }
2530
+ interface SomewhereJobsRecovery {
2531
+ dispatch_state: string;
2532
+ workflow_state?: string | null;
2533
+ recovery_error: string | null;
2534
+ observed_at?: number | null;
2535
+ }
2536
+ interface SomewhereJobsCreateResult {
2537
+ job_id: string;
2538
+ // 'indeterminate' means read recovery before retrying.
2539
+ status: string;
2540
+ duplicate: boolean;
2541
+ recovery: SomewhereJobsRecovery | null;
2542
+ ownership_status?: 'app_user' | 'project_owner';
2543
+ owner_subject_id?: string | null;
2544
+ }
2545
+ interface SomewhereJob {
2546
+ job_id: string;
2547
+ project_id: string;
2548
+ handler: string;
2549
+ status: string;
2550
+ progress: number;
2551
+ progress_message: string | null;
2552
+ payload: unknown;
2553
+ result: unknown;
2554
+ error: string | null;
2555
+ error_code: string | null;
2556
+ webhook_url: string | null;
2557
+ webhook_delivered: boolean;
2558
+ timeout_seconds: number;
2559
+ attempts: unknown[];
2560
+ priority: string;
2561
+ created_at: string;
2562
+ started_at: string | null;
2563
+ completed_at: string | null;
2564
+ last_heartbeat_at: string | null;
2565
+ ownership_status: 'app_user' | 'project_owner';
2566
+ owner_subject_id: string | null;
2567
+ cron_id: string | null;
2568
+ cron_scheduled_at: string | null;
2569
+ trigger: 'scheduled' | 'manual' | null;
2570
+ recovery: SomewhereJobsRecovery | null;
2571
+ }
2572
+ interface SomewhereRuntimeJobs {
2573
+ create(options: SomewhereJobsCreateOptions): Promise<SomewhereJobsCreateResult>;
2574
+ status(jobId: string): Promise<SomewhereJob>;
2575
+ // True only for a platform-signed job/queue/cron delivery; never throws.
2576
+ verifyInvocation(req: Request): Promise<boolean>;
2577
+ }
2578
+
2579
+ // \u2500\u2500 sw.cron \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2580
+ interface SomewhereCronCreateOptions {
2581
+ // 5-field cron expression, evaluated in timezone (UTC by default).
2582
+ schedule: string;
2583
+ handler: string;
2584
+ timezone?: string;
2585
+ payload?: unknown;
2586
+ name?: string;
2587
+ enabled?: boolean;
2588
+ }
2589
+ interface SomewhereCron {
2590
+ cron_id: string;
2591
+ project_id: string;
2592
+ name: string | null;
2593
+ schedule: string;
2594
+ timezone: string;
2595
+ handler: string;
2596
+ payload: unknown;
2597
+ enabled: boolean;
2598
+ last_run_at: string | null;
2599
+ last_run_status: string | null;
2600
+ last_run_job_id: string | null;
2601
+ last_error_code: string | null;
2602
+ last_error: string | null;
2603
+ consecutive_failures: number;
2604
+ paused_reason: string | null;
2605
+ next_run_at: string;
2606
+ created_at: string;
2607
+ }
2608
+ interface SomewhereCronPolicy {
2609
+ plan: string;
2610
+ enabled: boolean;
2611
+ min_interval_minutes: number | null;
2612
+ max_per_project: number | null;
2613
+ }
2614
+ interface SomewhereRuntimeCron {
2615
+ create(options: SomewhereCronCreateOptions): Promise<{ cron_id: string; schedule: string; timezone: string; next_run: string }>;
2616
+ list(): Promise<{ crons: SomewhereCron[]; policy: SomewhereCronPolicy }>;
2617
+ update(id: string, changes: Partial<SomewhereCronCreateOptions>): Promise<SomewhereCron>;
2618
+ delete(id: string): Promise<{ deleted: true; cron_id: string }>;
2619
+ }
2620
+
2621
+ // \u2500\u2500 sw.tasks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2622
+ type SomewhereTaskStatus = 'backlog' | 'open' | 'in_progress' | 'blocked' | 'needs_review' | 'done' | 'archived';
2623
+ type SomewhereTaskPriority = 'low' | 'normal' | 'high' | 'urgent';
2624
+ type SomewhereTaskHealth = 'on_track' | 'at_risk' | 'off_track';
2625
+ interface SomewhereTask {
2626
+ id: string;
2627
+ title: string;
2628
+ description: string | null;
2629
+ status: SomewhereTaskStatus;
2630
+ priority: SomewhereTaskPriority;
2631
+ type: string;
2632
+ assignee: string | null;
2633
+ reporter: string | null;
2634
+ labels: string[];
2635
+ due_at: number | null;
2636
+ area: string | null;
2637
+ parent_id: string | null;
2638
+ superseded_by: string | null;
2639
+ shipped_in: string | null;
2640
+ deployment_project_id: string | null;
2641
+ attachments: string[];
2642
+ status_note: string | null;
2643
+ health: SomewhereTaskHealth | null;
2644
+ status_note_updated_at: number | null;
2645
+ created_at: number;
2646
+ updated_at: number;
2647
+ completed_at: number | null;
2648
+ }
2649
+ interface SomewhereTaskListItem extends SomewhereTask {
2650
+ description_excerpt: string;
2651
+ description_truncated: boolean;
2652
+ comment_count: number;
2653
+ stale: boolean;
2654
+ }
2655
+ interface SomewhereTaskComment {
2656
+ id: string;
2657
+ task_id: string;
2658
+ author: string | null;
2659
+ body: string;
2660
+ created_at: number;
2661
+ [key: string]: unknown;
2662
+ }
2663
+ // Full view: the task plus comments, activity, relationships and closure guidance.
2664
+ interface SomewhereTaskDetail extends SomewhereTask {
2665
+ project_id: string;
2666
+ comments: SomewhereTaskComment[];
2667
+ activity: SomewhereJsonObject[];
2668
+ [key: string]: unknown;
2669
+ }
2670
+ interface SomewhereTaskCreateOptions {
2671
+ title: string;
2672
+ description?: string;
2673
+ status?: SomewhereTaskStatus;
2674
+ priority?: SomewhereTaskPriority;
2675
+ type?: string;
2676
+ assignee?: string | null;
2677
+ reporter?: string;
2678
+ labels?: readonly string[];
2679
+ due_at?: number | null;
2680
+ area?: string;
2681
+ parent_id?: string;
2682
+ // sw.fs paths.
2683
+ attachments?: string | readonly string[] | null;
2684
+ template?: string;
2685
+ }
2686
+ interface SomewhereTaskUpdateOptions {
2687
+ title?: string;
2688
+ description?: string | null;
2689
+ status?: SomewhereTaskStatus;
2690
+ priority?: SomewhereTaskPriority;
2691
+ type?: string;
2692
+ assignee?: string | null;
2693
+ labels?: readonly string[];
2694
+ due_at?: number | null;
2695
+ area?: string | null;
2696
+ parent_id?: string | null;
2697
+ superseded_by?: string | null;
2698
+ shipped_in?: string | null;
2699
+ status_note?: string | null;
2700
+ health?: SomewhereTaskHealth | null;
2701
+ attachments?: string | readonly string[] | null;
2702
+ append_attachments?: string | readonly string[];
2703
+ // Positive deploy version number; mutually exclusive with shipped_in.
2704
+ deployment_version?: number;
2705
+ deployment_project_id?: string;
2706
+ // Persisted as a comment; include it when closing a task.
2707
+ resolution_note?: string;
2708
+ comment?: string;
2709
+ }
2710
+ interface SomewhereTaskSettings {
2711
+ project_id: string;
2712
+ notify_email: string | null;
2713
+ webhook_url: string | null;
2714
+ webhook_configured: boolean;
2715
+ }
2716
+ interface SomewhereRuntimeTasks {
2717
+ // The created task, or only its id when the read-back was unavailable.
2718
+ create(task: SomewhereTaskCreateOptions): Promise<SomewhereTask | { id: string }>;
2719
+ // parent_id: 'null' lists only top-level tasks.
2720
+ list(options?: { status?: SomewhereTaskStatus; assignee?: string; area?: string; parent_id?: string | null; limit?: number } | null): Promise<SomewhereTaskListItem[]>;
2721
+ get(id: string): Promise<SomewhereTaskDetail>;
2722
+ update(id: string, changes: SomewhereTaskUpdateOptions): Promise<SomewhereTask>;
2723
+ delete(id: string): Promise<{ id: string; deleted: true }>;
2724
+ comment(id: string, body: string, author?: string): Promise<{ id: string; task_id: string; author: string; actor: string; body: string; created_at: number; edited_at: null }>;
2725
+ readonly settings: {
2726
+ get(): Promise<SomewhereTaskSettings>;
2727
+ // webhook_secret is returned once, when a webhook_url is first set.
2728
+ update(settings: { webhook_url?: string | null; notify_email?: string | null }): Promise<SomewhereTaskSettings & { webhook_secret: string | null }>;
2729
+ };
2730
+ }
2731
+
2732
+ // \u2500\u2500 sw.calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2733
+ interface SomewhereRuntimeCalls {
2734
+ newSession(options?: { thirdparty?: boolean } | null): Promise<{ session_id: string; project_id: string; authorization_status: 'persisted' }>;
2735
+ }
2736
+
2737
+ interface SomewhereRuntimeContext {
2738
+ readonly email: SomewhereRuntimeEmail;
2739
+ readonly contacts: SomewhereRuntimeContacts;
2740
+ readonly inbox: SomewhereRuntimeInbox;
2741
+ readonly notifications: SomewhereRuntimeNotifications;
2742
+ readonly push: SomewhereRuntimePush;
2743
+ readonly queue: SomewhereRuntimeQueue;
2744
+ readonly jobs: SomewhereRuntimeJobs;
2745
+ readonly cron: SomewhereRuntimeCron;
2746
+ readonly tasks: SomewhereRuntimeTasks;
2747
+ readonly calls: SomewhereRuntimeCalls;
2748
+ }
451
2749
  interface __SomewhereTypedRequest<Input> extends Request { json(): Promise<Input> }
452
2750
  type ServerFunction<Contract extends { input: unknown; output: unknown }> =
453
2751
  (req: __SomewhereTypedRequest<Contract["input"]>, sw: SomewhereRuntimeContext) =>
@@ -713,10 +3011,17 @@ function bakedRelationsFromDeclared(value) {
713
3011
  if (typeof rel.table !== "string" || !SAFE_SCOPE_IDENTIFIER.test(rel.table)) continue;
714
3012
  if (typeof rel.fk !== "string" || !SAFE_SCOPE_IDENTIFIER.test(rel.fk)) continue;
715
3013
  if (typeof rel.parentKey !== "string" || !SAFE_SCOPE_IDENTIFIER.test(rel.parentKey)) continue;
3014
+ if (rel.kind !== void 0 && rel.kind !== "hasMany" && rel.kind !== "belongsTo") continue;
716
3015
  const name = rel.name.toLowerCase();
717
3016
  if (seen.has(name)) continue;
718
3017
  seen.add(name);
719
- out.push({ name, table: rel.table.toLowerCase(), fk: rel.fk.toLowerCase(), parentKey: rel.parentKey.toLowerCase() });
3018
+ out.push({
3019
+ name,
3020
+ ...rel.kind === "belongsTo" ? { kind: "belongsTo" } : {},
3021
+ table: rel.table.toLowerCase(),
3022
+ fk: rel.fk.toLowerCase(),
3023
+ parentKey: rel.parentKey.toLowerCase()
3024
+ });
720
3025
  }
721
3026
  return out.length > 0 ? out : void 0;
722
3027
  }
@@ -747,6 +3052,63 @@ function bakedMemberFromDeclared(scope) {
747
3052
  return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {} };
748
3053
  }
749
3054
 
3055
+ // worker/src/utils/db-schema-deploy/extract-schema-relations.ts
3056
+ function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone, errors) {
3057
+ const columnByName = (t, col) => t.columns.find((c) => c.name === col);
3058
+ for (const t of tables) {
3059
+ if (t.relations.length === 0) continue;
3060
+ const rootNames = knownColumns(t);
3061
+ for (const rel of t.relations) {
3062
+ if (rel.name === "__proto__" || rel.name === "prototype" || rel.name === "constructor") {
3063
+ errors.push(
3064
+ `Relation "${t.name}"."${rel.name}" uses a reserved object name. Choose a relation name that can be returned as an ordinary row field.`
3065
+ );
3066
+ } else if (rootNames.has(rel.name)) {
3067
+ errors.push(
3068
+ `Relation "${t.name}"."${rel.name}" has the same name as a column on "${t.name}". Choose a different relation name so nesting related rows cannot overwrite the root column.`
3069
+ );
3070
+ }
3071
+ const related = tableByName.get(rel.table);
3072
+ if (!related) {
3073
+ const marked = markedGone(rel.table);
3074
+ errors.push(
3075
+ marked ? `Relation "${t.name}"."${rel.name}" targets "${rel.table}", which this file marks ${marked}. Remove the relation before the table can go.` : `Relation "${t.name}"."${rel.name}" targets ${rel.kind === "hasMany" ? "child" : "parent"} table "${rel.table}", which is not declared in db/schema.ts. A relation can only target a managed table in the same declaration.`
3076
+ );
3077
+ continue;
3078
+ }
3079
+ const keySide = rel.kind === "hasMany" ? t : related;
3080
+ const fkSide = rel.kind === "hasMany" ? related : t;
3081
+ const idCol = keySide.columns.find((c) => c.helper === "id");
3082
+ if (!idCol) {
3083
+ errors.push(
3084
+ rel.kind === "hasMany" ? `Table "${t.name}" declares relation "${rel.name}" but has no id() column to join on. A hasMany relation joins the child's foreign key to this table's primary key \u2014 declare an id() column.` : `Relation "${t.name}"."${rel.name}" belongsTo "${rel.table}", but "${rel.table}" has no id() column to join on. A belongsTo relation joins this table's foreign key to the parent's primary key \u2014 declare an id() column on "${rel.table}".`
3085
+ );
3086
+ } else {
3087
+ rel.parentKey = idCol.name;
3088
+ }
3089
+ const fkCol = columnByName(fkSide, rel.fk);
3090
+ if (!fkCol) {
3091
+ errors.push(
3092
+ `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but "${fkSide.name}" has no column "${rel.fk}". The foreign key must be a real column on "${fkSide.name}".`
3093
+ );
3094
+ continue;
3095
+ }
3096
+ if (fkCol.references !== keySide.name) {
3097
+ errors.push(
3098
+ fkCol.references === null ? `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but that column declares no foreign key. Declare it as ${rel.fk}: <type>({ references: '${keySide.name}' }) so the join key is a proven foreign key, not a free-form join.` : `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but that column references "${fkCol.references}", not "${keySide.name}". The foreign key must reference the table that holds the primary key of this relation.`
3099
+ );
3100
+ } else if (idCol) {
3101
+ const expectedHelper = idCol.uuid ? "text" : "integer";
3102
+ if (fkCol.helper !== expectedHelper) {
3103
+ errors.push(
3104
+ `Relation "${t.name}"."${rel.name}" joins "${fkSide.name}"."${rel.fk}" to "${keySide.name}"."${idCol.name}", but their key types do not match. Use ${expectedHelper}() for the foreign key so fetched rows can be stitched to the ${idCol.uuid ? "text" : "whole-number"} key exactly.`
3105
+ );
3106
+ }
3107
+ }
3108
+ }
3109
+ }
3110
+ }
3111
+
750
3112
  // worker/src/utils/db-schema-deploy/extract-schema-ts.ts
751
3113
  function policyOwner(policy) {
752
3114
  if (policy.kind === "owner") return policy;
@@ -1328,26 +3690,28 @@ function readRelations(r, tableName) {
1328
3690
  seen.add(name);
1329
3691
  r.expectPunct(":", `after relation "${key.name}" on table "${tableName}"`);
1330
3692
  const callee = r.expectIdent(`for relation "${key.name}" on table "${tableName}"`);
1331
- if (callee.value !== "hasMany") {
1332
- throw new SchemaTsError(`line ${callee.line}: relation "${key.name}" on table "${tableName}" uses "${callee.value}()", which is not supported. Only hasMany('child_table', 'foreign_key') is available.`);
3693
+ if (callee.value !== "hasMany" && callee.value !== "belongsTo") {
3694
+ throw new SchemaTsError(`line ${callee.line}: relation "${key.name}" on table "${tableName}" uses "${callee.value}()", which is not supported. Use hasMany('child_table', 'foreign_key') or belongsTo('parent_table', 'foreign_key').`);
1333
3695
  }
1334
- r.expectPunct("(", 'after "hasMany"');
1335
- const childTok = r.next();
1336
- if (childTok.kind !== "string") throw new SchemaTsError(`line ${childTok.line}: hasMany() for relation "${key.name}" on table "${tableName}" needs the child table name as a quoted string first.`);
1337
- r.expectPunct(",", `after the child table in hasMany() for relation "${key.name}" on table "${tableName}" \u2014 hasMany('child_table', 'foreign_key')`);
3696
+ const kind = callee.value;
3697
+ const side = kind === "hasMany" ? "child" : "parent";
3698
+ r.expectPunct("(", `after "${kind}"`);
3699
+ const relatedTok = r.next();
3700
+ if (relatedTok.kind !== "string") throw new SchemaTsError(`line ${relatedTok.line}: ${kind}() for relation "${key.name}" on table "${tableName}" needs the ${side} table name as a quoted string first.`);
3701
+ r.expectPunct(",", `after the ${side} table in ${kind}() for relation "${key.name}" on table "${tableName}" \u2014 ${kind}('${side}_table', 'foreign_key')`);
1338
3702
  const fkTok = r.next();
1339
- if (fkTok.kind !== "string") throw new SchemaTsError(`line ${fkTok.line}: hasMany() for relation "${key.name}" on table "${tableName}" needs the child foreign-key column as a quoted string second.`);
3703
+ if (fkTok.kind !== "string") throw new SchemaTsError(`line ${fkTok.line}: ${kind}() for relation "${key.name}" on table "${tableName}" needs the foreign-key column as a quoted string second.`);
1340
3704
  r.tryPunct(",");
1341
- r.expectPunct(")", `closing hasMany() for relation "${key.name}" on table "${tableName}"`);
1342
- const childTable = childTok.value.toLowerCase();
3705
+ r.expectPunct(")", `closing ${kind}() for relation "${key.name}" on table "${tableName}"`);
3706
+ const relatedTable = relatedTok.value.toLowerCase();
1343
3707
  const fk = fkTok.value.toLowerCase();
1344
- if (!SAFE_IDENT.test(childTable) || childTable.length > MAX_NAME_LENGTH) {
1345
- throw new SchemaTsError(`line ${childTok.line}: hasMany() child table "${childTok.value}" for relation "${key.name}" on table "${tableName}" is not a valid table name.`);
3708
+ if (!SAFE_IDENT.test(relatedTable) || relatedTable.length > MAX_NAME_LENGTH) {
3709
+ throw new SchemaTsError(`line ${relatedTok.line}: ${kind}() ${side} table "${relatedTok.value}" for relation "${key.name}" on table "${tableName}" is not a valid table name.`);
1346
3710
  }
1347
3711
  if (!SAFE_IDENT.test(fk) || fk.length > MAX_NAME_LENGTH) {
1348
- throw new SchemaTsError(`line ${fkTok.line}: hasMany() foreign key "${fkTok.value}" for relation "${key.name}" on table "${tableName}" is not a valid column name.`);
3712
+ throw new SchemaTsError(`line ${fkTok.line}: ${kind}() foreign key "${fkTok.value}" for relation "${key.name}" on table "${tableName}" is not a valid column name.`);
1349
3713
  }
1350
- out.push({ name, table: childTable, fk, parentKey: "" });
3714
+ out.push({ name, kind, table: relatedTable, fk, parentKey: "" });
1351
3715
  if (!r.tryPunct(",")) {
1352
3716
  r.expectPunct("}", `closing "relations" of table "${tableName}"`);
1353
3717
  break;
@@ -1801,57 +4165,7 @@ function extractSchemaTs(source) {
1801
4165
  }
1802
4166
  }
1803
4167
  }
1804
- const columnByName = (t, col) => t.columns.find((c) => c.name === col);
1805
- for (const t of tables) {
1806
- if (t.relations.length === 0) continue;
1807
- const idCol = t.columns.find((c) => c.helper === "id");
1808
- const rootNames = knownColumns(t);
1809
- for (const rel of t.relations) {
1810
- if (rel.name === "__proto__" || rel.name === "prototype" || rel.name === "constructor") {
1811
- errors.push(
1812
- `Relation "${t.name}"."${rel.name}" uses a reserved object name. Choose a relation name that can be returned as an ordinary row field.`
1813
- );
1814
- } else if (rootNames.has(rel.name)) {
1815
- errors.push(
1816
- `Relation "${t.name}"."${rel.name}" has the same name as a column on "${t.name}". Choose a different relation name so nesting child rows cannot overwrite the root column.`
1817
- );
1818
- }
1819
- if (!idCol) {
1820
- errors.push(
1821
- `Table "${t.name}" declares relation "${rel.name}" but has no id() column to join on. A hasMany relation joins the child's foreign key to this table's primary key \u2014 declare an id() column.`
1822
- );
1823
- } else {
1824
- rel.parentKey = idCol.name;
1825
- }
1826
- const child = tableByName.get(rel.table);
1827
- if (!child) {
1828
- const marked = removedTables.includes(rel.table) || exportedTables.includes(rel.table);
1829
- errors.push(
1830
- marked ? `Relation "${t.name}"."${rel.name}" targets "${rel.table}", which this file marks ${removedTables.includes(rel.table) ? "removedTable()" : "exported()"}. Remove the relation before the table can go.` : `Relation "${t.name}"."${rel.name}" targets child table "${rel.table}", which is not declared in db/schema.ts. A relation can only target a managed table in the same declaration.`
1831
- );
1832
- continue;
1833
- }
1834
- const fkCol = columnByName(child, rel.fk);
1835
- if (!fkCol) {
1836
- errors.push(
1837
- `Relation "${t.name}"."${rel.name}" joins on "${rel.table}"."${rel.fk}", but "${rel.table}" has no column "${rel.fk}". The foreign key must be a real column on the child table.`
1838
- );
1839
- continue;
1840
- }
1841
- if (fkCol.references !== t.name) {
1842
- errors.push(
1843
- fkCol.references === null ? `Relation "${t.name}"."${rel.name}" joins on "${rel.table}"."${rel.fk}", but that column declares no foreign key. Declare it as ${rel.fk}: <type>({ references: '${t.name}' }) so the join key is a proven foreign key, not a free-form join.` : `Relation "${t.name}"."${rel.name}" joins on "${rel.table}"."${rel.fk}", but that column references "${fkCol.references}", not "${t.name}". The foreign key must reference the table that declares the relation.`
1844
- );
1845
- } else if (idCol) {
1846
- const expectedHelper = idCol.uuid ? "text" : "integer";
1847
- if (fkCol.helper !== expectedHelper) {
1848
- errors.push(
1849
- `Relation "${t.name}"."${rel.name}" joins "${rel.table}"."${rel.fk}" to "${t.name}"."${idCol.name}", but their key types do not match. Use ${expectedHelper}() for the foreign key so fetched child rows can be stitched to the ${idCol.uuid ? "text" : "whole-number"} parent id exactly.`
1850
- );
1851
- }
1852
- }
1853
- }
1854
- }
4168
+ validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
1855
4169
  if (errors.length > 0) return { ok: false, errors };
1856
4170
  return {
1857
4171
  ok: true,
@@ -1892,8 +4206,18 @@ function canonicalTableShape(t) {
1892
4206
  uniques: t.uniques,
1893
4207
  ...t.removedColumns.length > 0 ? { removedColumns: t.removedColumns } : {},
1894
4208
  // conditional: a relation-free table keeps its slice-1 generation id. Ordered
1895
- // by name (readRelations sorts) so the shape is deterministic.
1896
- ...t.relations.length > 0 ? { relations: t.relations.map((rel) => ({ name: rel.name, table: rel.table, fk: rel.fk, parentKey: rel.parentKey })) } : {}
4209
+ // by name (readRelations sorts) so the shape is deterministic. `kind` is
4210
+ // likewise conditional — absent means hasMany, so a declaration that uses
4211
+ // only hasMany keeps the generation id the composed-joins slice minted.
4212
+ ...t.relations.length > 0 ? {
4213
+ relations: t.relations.map((rel) => ({
4214
+ name: rel.name,
4215
+ table: rel.table,
4216
+ fk: rel.fk,
4217
+ parentKey: rel.parentKey,
4218
+ ...rel.kind === "belongsTo" ? { kind: "belongsTo" } : {}
4219
+ }))
4220
+ } : {}
1897
4221
  };
1898
4222
  }
1899
4223