@somewhere-cloud/cli 0.38.17 → 0.38.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.18
4
+
5
+ ### Changed
6
+
7
+ - `somewhere typecheck` and `somewhere lint` use the platform's current
8
+ schema parser, `somewhere/db` types and lint rules (tech c077686c).
9
+ `sw.db.insert(table, values, { onConflict: 'ignore' | 'update' })` and
10
+ `sw.db.tx` insert intents with the same `options` now type on every table a
11
+ signed-in caller writes: `owner()`, `shared()`, `member()`, `group()`,
12
+ `parent()`, `anyOf(owner(), …)` including `grant()`, and `allOf()`.
13
+ `onConflict: 'update'` is a type error where the platform refuses it: a
14
+ table whose rule never grants update, a per-row `grant()` shares table, a
15
+ table named `excluded`, and an insert with `idempotencyKey`.
16
+
3
17
  ## 0.38.17
4
18
 
5
19
  ### Fixed
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@somewhere-cloud/cli",
3
- "version": "0.38.17",
3
+ "version": "0.38.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@somewhere-cloud/cli",
9
- "version": "0.38.17",
9
+ "version": "0.38.18",
10
10
  "bundleDependencies": [
11
11
  "@modelcontextprotocol/sdk",
12
12
  "chokidar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somewhere-cloud/cli",
3
- "version": "0.38.17",
3
+ "version": "0.38.18",
4
4
  "description": "CLI for somewhere.cloud — auth, projects, deploy, pull, promote, db, logs, env, MCP bridge.",
5
5
  "keywords": [
6
6
  "somewhere",
@@ -2,11 +2,11 @@
2
2
  "esbuild": "0.24.0",
3
3
  "source_files": {
4
4
  "worker/containers/compile/checker-contract.cjs": "3a8355b9477d89d00bca47cf74032c3b3c6458941cf5c57ddae9e2228b51915d",
5
- "worker/containers/compile/declared-data-contract.cjs": "6d421db4a64f7f3ea58ec7bf773a1090a0f04bc99d59219178f3b1b602ad3d68",
5
+ "worker/containers/compile/declared-data-contract.cjs": "4c853d6a21def9d4443f3e8873d177dbbe4e957457e8293cd183830a81727565",
6
6
  "worker/containers/compile/generated-declarations-render.cjs": "7ffdb34645cc9eccac6eba39dc86b646a41c4de8a1a038d2b5b46f42df9f1157",
7
7
  "worker/containers/compile/generated-declarations.cjs": "6e9902e054c5c784fd9b3c3fafc1869e79954100a45014e1132247cd82833ec4",
8
8
  "worker/containers/compile/groups-types.cjs": "7c22b4bb96c9391005d434852600dc02bc8a6bbb99d78d2b8e3eb152f7b72c58",
9
- "worker/containers/compile/runtime-types.cjs": "1acc241c97efe2e0a33853bc3a01fbf48e1e9b6e13d9310f42035aac1e1e625b",
9
+ "worker/containers/compile/runtime-types.cjs": "fa9767ee2a0727511701821d0c18c878b7ce7b9ab3e92674057b474ad25e548e",
10
10
  "worker/containers/compile/schema-types.cjs": "aa8987178dcea6bdee593e7ff06f452265b2af9d12d1bd1c47c983db15965a73",
11
11
  "worker/containers/compile/typed-data.cjs": "1c8bec853fbee4ffd34c651076a4e1f4d8b18c0c2be5e99e31df7c9283207627",
12
12
  "worker/containers/compile/typed-files.cjs": "a0b1818afbc7673f84583c76436a3b3ca1d1fdf63d8d8db138b36646103f0708",
@@ -36,5 +36,5 @@
36
36
  "worker/src/utils/managed-groups/policy.ts": "2c5b48479a2925bea9a820022c63ca5fa827e2ff5531fe1ab6c0b058b7212b39",
37
37
  "worker/src/utils/reference-check-inputs.ts": "59c7c8d3b4aaf8bffb2a54964e41f909437745d001b093edd74cc135f4edcf67"
38
38
  },
39
- "sha256": "b88c7de7c2393bb422722885eb5b677541b5adde856fe0c511db7aa940453a5b"
39
+ "sha256": "c62061ce7285f32fa53bf1cef84b3455bd13d0af05f2bbbc1af1b8abf9de9f09"
40
40
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "repository": "Somewhere-Cloud/somewhere-tech",
3
- "commit": "902bf71b49d9b53eab7a6fd9b5de6bc67a4b7c1b"
3
+ "commit": "c077686c7fc94549f6fa3fa2b52b8315f58abd40"
4
4
  }
@@ -6,7 +6,7 @@
6
6
  "shared/d1-session.ts": "2ae29b74bba68016c86d25fa2a58f8c2fce4aca8fe57fafc253d380c47a3c0ab",
7
7
  "shared/draft-execution-authority.ts": "607db6162732cc55798f1b871103c49b246f6667c1e4c7526abd85416a50abf6",
8
8
  "shared/public-result.ts": "85ba1a6dd4136fd7362cf7f38b3042b1cacfc389af8c28343398c1ba83d26298",
9
- "worker/containers/compile/declared-data-contract.cjs": "6d421db4a64f7f3ea58ec7bf773a1090a0f04bc99d59219178f3b1b602ad3d68",
9
+ "worker/containers/compile/declared-data-contract.cjs": "4c853d6a21def9d4443f3e8873d177dbbe4e957457e8293cd183830a81727565",
10
10
  "worker/containers/compile/function-route.cjs": "e7faf1104ba95e27921901b4619bd98aa6a2be47cc10af00097fd4957744606b",
11
11
  "worker/node_modules/acorn/dist/acorn.mjs": "b4c8c70200e72bae33cf1085e0ecb1e792c1b6924ed50cab817caf14f51bb249",
12
12
  "worker/src/runtime/access-token.ts": "f6ab74f3a9e0bbfb15ac5a6a2f7391f4fb1e4e03b7358fbf8b61d962d14582ef",
@@ -30,7 +30,7 @@
30
30
  "worker/src/runtime/crypto.ts": "5c91be24e7da79a8c11f04e06f70158ce89e09940a0918711c656d4cc893915d",
31
31
  "worker/src/runtime/db-aggregate.ts": "e4c13540182e5990c49f5bd8e3ee53ca387ef2c87787ebc4daf8406d13e9375a",
32
32
  "worker/src/runtime/db-coalesce.ts": "260fd91b0741f709c53005036528ac77669810296c4f82294bdef8cff6a36fdc",
33
- "worker/src/runtime/db-composed-writes.ts": "ccdbb6042a9512e2fc279d56722fea23c16aa6095a7c55a536cd344232b3eb33",
33
+ "worker/src/runtime/db-composed-writes.ts": "9c0fb6e8118cddf5dab51a2c5e2250a74097d4515349777d86365400943d0b7b",
34
34
  "worker/src/runtime/db-generated-owner-read.ts": "875a1ed906272dc03b4fa9f73ecf9621e09b6c5e3b52905de70d0ae9861f8bec",
35
35
  "worker/src/runtime/db-intake.ts": "d7b7f4b5865f002c741a06c8214af4e54806295826ab69575ed9c403100f0752",
36
36
  "worker/src/runtime/db-live-invalidation.ts": "7697afe746de632cf8669dec657e340185e989ec87c76322ff0755f0b5d8b06f",
@@ -43,8 +43,8 @@
43
43
  "worker/src/runtime/db-result-fidelity.ts": "224ae06727afcccd648ed73cffb4216321542e53d2bf7a7212dd0c5d00792f4e",
44
44
  "worker/src/runtime/db-row-links.ts": "af41e679181cb4ecefb2df04f0d8f91f1f23abf50d0594c347fff15d59d4cb66",
45
45
  "worker/src/runtime/db-session-recovery.ts": "7115703aad233f46295915273b05d04f4848ebe05ff1ec0f25f5ce5d298b418e",
46
- "worker/src/runtime/db-structured-execution.ts": "074b32646ea6648c79cac75100282785153a9eb3003ce84c397b2734c355d2bc",
47
- "worker/src/runtime/db-typed-errors.ts": "be6d7ffb72b9a7e749207455191444108ed9c4ec38ea670f845f0fa249a5dff8",
46
+ "worker/src/runtime/db-structured-execution.ts": "c562aa1cbfdc894af5ea6d064f888b1b08eaa0fdd0a6a3ffdfd132ce78ed7083",
47
+ "worker/src/runtime/db-typed-errors.ts": "508f0cc79e85f6722c6696f777f4dfdf6e65ac8ff17157a8ae9b5358377c20b3",
48
48
  "worker/src/runtime/db.ts": "87e7f7c6c6f4491ef36f85e6a23c9a612c09297ce1d0ffaabf281fca0d26728a",
49
49
  "worker/src/runtime/declared-data-read-plan.ts": "e1b446eea9d274099e2151e280e115d692fc59e94f0f0c872ab9defcb1dfec5c",
50
50
  "worker/src/runtime/declared-data.ts": "011cc2c747942bed9fd03b921cf2a5da91c881771821c78771e6ee78346b67f7",
@@ -72,7 +72,7 @@
72
72
  "worker/src/runtime/postgres.ts": "168aafb45795dfec8796cc133e0f4d057cc9a917d846b4ca653b85e8eabf1498",
73
73
  "worker/src/runtime/push-client.ts": "d8bf70c34336d22edeaeae9976ed3428fa18530bb4766718f8be439497dec0a4",
74
74
  "worker/src/runtime/push.ts": "21b08bd7d3a3df2bd0c54fd393ed4ab2f898dab575d7b640320a8f4914c0932f",
75
- "worker/src/runtime/query-sql.ts": "f0668557f7328dace4c4dfaedbd62f6f8a9ca4248f7a823e8058bfa663a31afa",
75
+ "worker/src/runtime/query-sql.ts": "dd13ee3474992131795dae65a36f79d3e3540e8c23a71035b58d0c9558006edd",
76
76
  "worker/src/runtime/queue.ts": "8f0bdda9bb857125d42cfbe0a4a50561daca56cc929f17bc91e1d4ba151cc998",
77
77
  "worker/src/runtime/rate-limit.ts": "a63481a2144a46f39228b3e22b2c18df6e4f02d3ae4fac928cd2ee708cc1d1c8",
78
78
  "worker/src/runtime/render.ts": "cbb484f18e410d26fcc87db259e1d10a1b18308405256b6c1a26afc2985707ed",
@@ -83,7 +83,7 @@
83
83
  "worker/src/runtime/storage.ts": "15c7953b994497c7883b774f79ae93bae96e16373a0826cc733d21005de22ad7",
84
84
  "worker/src/runtime/tasks.ts": "75efa2e3083853bfba1dc49745ede8865ddd218b84d568a00da2dbd669f9ab23",
85
85
  "worker/src/runtime/verify-signup.ts": "dcac46b4581794fe875e3f85e3d93a2253b470f60a3c7fcee68543d8be8a6fa1",
86
- "worker/src/runtime/version.ts": "19100ec841397d56b281947a9138e092785a99032423d271db6e117010e88b26",
86
+ "worker/src/runtime/version.ts": "1ab816ccce86c94439957c91a1ea28f3860ce7c48cd52c6d5e6a403f12cdc70c",
87
87
  "worker/src/runtime/visitor-session.generated.ts": "9753bc6c195a0072c21102d6caa853990ece23e80971b8726d311bdb93970b90",
88
88
  "worker/src/runtime/web.ts": "269590d44ffa6239bb6e79999b58ba42435c230f045daea52bf73d09519afb0e",
89
89
  "worker/src/security/browser-secret-scan.ts": "31ebc9dd94eed4d0a9a6b67b3b1e2505d4f7812c557c90337e034c1e3994c039",
@@ -144,6 +144,6 @@
144
144
  "worker/src/utils/sql-tokens.ts": "ac643c765def38905a0600ae155ad909335cae1a7c1912da7bc87af25acb752e",
145
145
  "worker/src/vendor/neondatabase-serverless/source.generated.ts": "96a3c18fde7d8888fdb53a4d7cd0721c238a234521d2084d1e66868a8df8e69c"
146
146
  },
147
- "sha256": "63941fff948cab385cbf64c59297e5abca4a63bf1bdfe9b29523e5423227c375",
147
+ "sha256": "0d11155b9a8bee2eab24f24c41a384ace071e994f6f925eebae2a4be16112cd2",
148
148
  "fixtures_sha256": "908827d1430f4af5a83cae59ed476abc51dc9648757eac0089fe6e6db95f66c5"
149
149
  }
@@ -684,7 +684,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
684
684
  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}}
685
685
  const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
686
686
  let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;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};
687
- ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}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"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values=>invoke(table,operation,{values})"}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}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))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
687
+ ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}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"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}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))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
688
688
  export{data,DataError};
689
689
  `;
690
690
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
@@ -963,6 +963,13 @@ interface SomewhereDbPlainInsertOptions { onConflict?: 'ignore' | 'update' | nul
963
963
  // signed-in user or visitor on a table keyed by id() with no blob() column. Any other insert
964
964
  // given a key refuses before anything runs. Eligible inserts without one get a fresh key.
965
965
  interface SomewhereDbInsertOptions extends SomewhereDbPlainInsertOptions { idempotencyKey?: string }
966
+ // A caller upsert runs under the table's declared rule: 'update' also needs update on the conflicting
967
+ // row, so a declared table whose rule never grants update (or a per-row shares table) takes 'ignore'
968
+ // only. A key never combines with onConflict.
969
+ type SomewhereDbConflict<T extends string> = __SomewhereDeclared<T, 'conflict', 'ignore' | 'update'>;
970
+ type SomewhereDbCallerInsertOptions<T extends string> =
971
+ | { onConflict?: SomewhereDbConflict<T> | null; idempotencyKey?: never }
972
+ | { onConflict?: null; idempotencyKey?: string };
966
973
  interface SomewhereDbReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number }
967
974
  // Process-local: retry() repeats this exact request with the same key under the current
968
975
  // context's authority, until eight days after createdAt. It never runs by itself.
@@ -980,7 +987,7 @@ interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
980
987
  // (insert values, update set/increment). With no declared tables every name and
981
988
  // value stays accepted, as before. Insert and Set pick caller or server identity.
982
989
  type __SomewhereDbIntentFor<T extends string, Insert extends 'insert' | 'serverInsert', Set extends 'set' | 'serverSet'> =
983
- | { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: SomewhereDbPlainInsertOptions | null }
990
+ | { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: (Insert extends 'insert' ? { onConflict?: SomewhereDbConflict<T> | null } : SomewhereDbPlainInsertOptions) | null }
984
991
  | ({ op: 'update'; table: T } & SomewhereDbUpdateFor<T, Set>)
985
992
  | { op: 'remove'; table: T; where?: SomewhereDbWhere | null };
986
993
  // sw.db.server.tx: server authority may name row identity (owner column, shared authorship).
@@ -1066,7 +1073,7 @@ interface SomewhereServerDb {
1066
1073
  }
1067
1074
  interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update' | 'tx'> {
1068
1075
  // The platform composes row identity from the verified user.
1069
- insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbInsertOptions | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
1076
+ insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbCallerInsertOptions<T> | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
1070
1077
  update<T extends SomewhereDbTable>(table: T, spec: SomewhereDbUpdateFor<T, 'set'>): Promise<SomewhereDbResult<SomewhereDbRow<T>>>;
1071
1078
  tx(intents: readonly SomewhereDbCallerWriteIntent[]): Promise<SomewhereDbResult[]>;
1072
1079
  from<T extends SomewhereDbTable, const C extends readonly SomewhereDbColumn<T>[] = readonly SomewhereDbColumn<T>[], const I extends readonly SomewhereDbRelationName<T>[] = []>(
@@ -3841,11 +3848,26 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
3841
3848
  const find = (policy) => !policy ? null : policy.k === "o" ? policy.c : policy.k === "a" ? find(policy.p[0]) || find(policy.p[1]) : policy.k === "n" ? find(policy.w) : null;
3842
3849
  return find(table.policy);
3843
3850
  }
3851
+ function declaredRuleGrants(policy, op) {
3852
+ if (!policy) return true;
3853
+ if (policy.k === "o") return true;
3854
+ if (policy.k === "m") return !policy.m.o || policy.m.o.includes(op);
3855
+ if (policy.k === "a") return declaredRuleGrants(policy.p[0], op) || declaredRuleGrants(policy.p[1], op);
3856
+ if (policy.k === "n") return declaredRuleGrants(policy.g, op) && declaredRuleGrants(policy.w, op) && (op !== "update" || declaredRuleGrants(policy.g, "read") && declaredRuleGrants(policy.w, "read"));
3857
+ if (policy.k === "g" || policy.k === "r") return !!(policy.roles && policy.roles[op] && policy.roles[op].length);
3858
+ if (policy.k === "p") return declaredRuleGrants(policy.p, op);
3859
+ return true;
3860
+ }
3844
3861
  function declaredTablesDeclaration3(authority) {
3845
3862
  const schema = authority && authority.schema || {};
3846
3863
  const scopes = authority && authority.scopes || {};
3847
3864
  const names = Object.keys(schema).sort();
3848
3865
  if (!names.length) return "";
3866
+ const rowShares = /* @__PURE__ */ new Set();
3867
+ for (const name of names) {
3868
+ const policy = schema[name].policy, branch = policy && policy.k === "a" ? policy.p[1] : null;
3869
+ if (branch && branch.k === "m" && branch.m.og && branch.m.g.length === 2) rowShares.add(String(branch.m.m).toLowerCase());
3870
+ }
3849
3871
  const key = JSON.stringify;
3850
3872
  const shape = (fields) => fields.length ? "{ " + fields.join("; ") + " }" : "Record<string, never>";
3851
3873
  const union = (columns) => columns.length ? columns.map((column) => key(column.n)).join(" | ") : "never";
@@ -3863,7 +3885,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
3863
3885
  if (owner && !columns.some((column) => column.n === owner)) columns.push({ n: owner, t: "text", optional: false, identity: true, type: "string" });
3864
3886
  const declared = columns.filter((column) => !column.identity);
3865
3887
  const relations = (table.relations || []).filter((relation) => Object.prototype.hasOwnProperty.call(schema, relation.table)).map((relation) => key(relation.name) + ": SomewhereDeclaredTables[" + key(relation.table) + "]['row']" + (relation.kind === "belongsTo" ? " | null" : "[]"));
3866
- return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n };\n";
3888
+ return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n conflict: " + (rowShares.has(name.toLowerCase()) || !declaredRuleGrants(table.policy || (table.member ? { k: "m", m: table.member } : null), "update") ? "'ignore'" : "'ignore' | 'update'") + ";\n };\n";
3867
3889
  });
3868
3890
  return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
3869
3891
  }
@@ -729,7 +729,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
729
729
  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}}
730
730
  const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
731
731
  let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;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};
732
- ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}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"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values=>invoke(table,operation,{values})"}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}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))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
732
+ ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}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"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}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))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
733
733
  export{data,DataError};
734
734
  `;
735
735
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
@@ -1547,6 +1547,8 @@ var DB_TYPED_ERRORS_JS = ` // The REST bridge's refusal as the caller sees
1547
1547
  // move only a grant() would allow.
1548
1548
  const refused = new Error(label !== 'sw.db.tx' && guard[1] === 'POLICY_AUTHORITY_REQUIRED'
1549
1549
  ? label + ': the access policy of this table does not permit this change by the signed-in user. Nothing was written.'
1550
+ : label !== 'sw.db.tx'
1551
+ ? label + ': this row targets a group the signed-in user is not a member of. Nothing was written.'
1550
1552
  : guard[1] === 'MEMBERSHIP_REQUIRED'
1551
1553
  ? 'sw.db.tx: an insert targets a group the signed-in user is not a member of. Nothing in the transaction was written.'
1552
1554
  : 'sw.db.tx: the access policy of a table does not permit one of these changes by the signed-in user. Nothing in the transaction was written.');
@@ -4773,7 +4775,8 @@ var DB_STRUCTURED_EXECUTION_JS = ` async function __sw_structExec(label, ir
4773
4775
  ? 'sw.db.server.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row its user_id owns.'
4774
4776
  : 'sw.db.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row the signed-in user owns.');
4775
4777
  }
4776
- if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && rows.length === 0) {
4778
+ // An upsert's refusals raise inside the statement (query-sql.ts); its empty result is an ignored conflict.
4779
+ if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && ir.onConflict === undefined && rows.length === 0) {
4777
4780
  __sw_structThrow403(ir.scope.mode === 'policy' ? 'POLICY_AUTHORITY_REQUIRED' : 'MEMBERSHIP_REQUIRED',
4778
4781
  ir.scope.mode === 'policy' ? 'sw.db.insert on "' + ir.table.name + '": the proposed row is outside your declared private policy.'
4779
4782
  : 'sw.db.insert on "' + ir.table.name + '": you can only add rows to a group you belong to, and the signed-in user is not a member of the group named in this row. Add the user to the group first, or write to a group they belong to.');
@@ -5807,6 +5810,44 @@ function renderQuerySql(ir, principalId, returning, context) {
5807
5810
  policyInsertPredicate(policy, proposedValues) {
5808
5811
  return helpers.policySql(context.renderPolicy({ render: "insert", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: proposedValues }));
5809
5812
  },
5813
+ // The columns that place a row under its rule and never change through an upsert: member()
5814
+ // group keys, a group() column, an allOf gate key, the parent() link and a per-row grant's
5815
+ // row key. The owner is stamped and never assigned, so it is not listed.
5816
+ policyFixedKeys(policy) {
5817
+ const keys = [];
5818
+ if (policy.k === "m") {
5819
+ if (!policy.m.og) for (let i = 0; i < policy.m.g.length; i++) keys.push(policy.m.g[i]);
5820
+ else if (policy.m.g.length === 2) keys.push(policy.m.g[1]);
5821
+ }
5822
+ if (policy.k === "g") keys.push(policy.c);
5823
+ if (policy.k === "p") keys.push(policy.v);
5824
+ if (policy.k === "a" || policy.k === "n") {
5825
+ const parts = policy.k === "a" ? policy.p : [policy.g, policy.w];
5826
+ for (let i = 0; i < parts.length; i++) {
5827
+ const inner = helpers.policyFixedKeys(parts[i]);
5828
+ for (let j = 0; j < inner.length; j++) keys.push(inner[j]);
5829
+ }
5830
+ }
5831
+ return keys;
5832
+ },
5833
+ // Upsert under a private rule (tsk_c98959e4): the conflicting existing row must pass the rule's
5834
+ // UPDATE predicate (rendered by the caller, binds already pushed) and keep every fixed key the
5835
+ // caller names. Otherwise the statement raises, so it is refused whole (a transaction rolls back),
5836
+ // never skipped.
5837
+ conflictUpdateSql(updatePredicate, fixed) {
5838
+ const sets = context.map(ir.values, function(e) {
5839
+ return q(e.column) + " = excluded." + q(e.column);
5840
+ });
5841
+ if (sets.length === 0) context.fail('sw.db.insert with { onConflict: "update" } needs at least one column value to update on conflict.');
5842
+ const held = ["(" + updatePredicate + ")"];
5843
+ for (let i = 0; i < fixed.length; i++) {
5844
+ for (let j = 0; j < ir.values.length; j++) {
5845
+ const column = ir.values[j].column;
5846
+ if (context.lower(column) === context.lower(fixed[i])) held.push(tableSql + "." + q(column) + " IS excluded." + q(column));
5847
+ }
5848
+ }
5849
+ return " ON CONFLICT DO UPDATE SET " + sets.join(", ") + " WHERE CASE WHEN " + held.join(" AND ") + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END";
5850
+ },
5810
5851
  // Row-link authority (tsk_f1529568): ONE correlated EXISTS proving, in the
5811
5852
  // statement itself, that the session token is live, belongs to this exact
5812
5853
  // grant, the grant is unrevoked and unexpired, names this table and THIS
@@ -5964,6 +6005,8 @@ function renderQuerySql(ir, principalId, returning, context) {
5964
6005
  };
5965
6006
  const q = context.quote;
5966
6007
  const tableSql = helpers.table(ir.table.name);
6008
+ if (ir.kind === "insert" && ir.onConflict === "update" && context.lower(tableSql) === '"excluded"')
6009
+ context.fail('sw.db.insert: a table named "excluded" cannot take onConflict: "update". Rename the table, or use onConflict: "ignore".');
5967
6010
  const policyOperation = ir.kind === "select" || ir.kind === "count" || ir.kind === "aggregate" ? "read" : ir.kind === "insert" ? "create" : ir.kind;
5968
6011
  let sql;
5969
6012
  if (ir.kind === "select") {
@@ -6092,8 +6135,9 @@ function renderQuerySql(ir, principalId, returning, context) {
6092
6135
  existsSql += " AND " + q(m.mg[i]) + " = ?";
6093
6136
  params.push(gv);
6094
6137
  }
6095
- const guard = ir.txGuard ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
6096
- sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
6138
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
6139
+ const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.memberScopePredicate(m, null, "update"), m.g) : "";
6140
+ sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
6097
6141
  return { sql, params };
6098
6142
  }
6099
6143
  if (ir.scope.mode === "link") {
@@ -6124,8 +6168,9 @@ function renderQuerySql(ir, principalId, returning, context) {
6124
6168
  for (let i = 0; i < ivals.length; i++)
6125
6169
  params.push(ivals[i]);
6126
6170
  const authorized = helpers.policyInsertPredicate(ir.scope.policy, ir.values || []);
6127
- const guard = ir.txGuard ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
6128
- sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
6171
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
6172
+ const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.policyPredicate(ir.scope.policy, null, null, "update"), helpers.policyFixedKeys(ir.scope.policy)) : "";
6173
+ sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
6129
6174
  return { sql, params };
6130
6175
  }
6131
6176
  if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) {
@@ -6167,13 +6212,14 @@ function renderQuerySql(ir, principalId, returning, context) {
6167
6212
  if (ir.onConflict === "ignore") {
6168
6213
  sql += " ON CONFLICT DO NOTHING";
6169
6214
  } else if (ir.onConflict === "update") {
6170
- const sets = context.map(ir.values, function(e) {
6171
- return q(e.column) + " = excluded." + q(e.column);
6172
- });
6173
- sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
6174
6215
  if (ir.scope.mode === "user") {
6175
- sql += " WHERE " + q(ir.scope.ownerColumn) + " = ?";
6176
6216
  params.push(principalId);
6217
+ sql += helpers.conflictUpdateSql(q(ir.scope.ownerColumn) + " = ?", []);
6218
+ } else {
6219
+ const sets = context.map(ir.values, function(e) {
6220
+ return q(e.column) + " = excluded." + q(e.column);
6221
+ });
6222
+ sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
6177
6223
  }
6178
6224
  }
6179
6225
  sql += context.returningSql(returning);
@@ -9759,14 +9805,14 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9759
9805
  if (decided.scope.mode === 'user') __sw_structAssertNotOwnerColumn(cols, decided.scope.ownerColumn, 'insert');
9760
9806
  if (decided.scope.mode === 'policy') __sw_structAssertPolicyWrite(cols, decided.scope.policy, 'insert');
9761
9807
  if (decided.scope.mode === 'policy') __sw_structAssertPolicyInsert(cols, decided.scope.policy);
9762
- if (decided.scope.mode === 'policy' && onConflict !== undefined) {
9763
- __sw_structThrow('sw.db.insert on policy-scoped tables does not support onConflict yet. Insert without onConflict.');
9764
- }
9765
- if (decided.scope.mode === 'member') {
9766
- if (onConflict !== undefined) {
9767
- __sw_structThrow('sw.db.insert on "' + table + '": onConflict is not supported on membership-scoped tables yet. Insert without onConflict.');
9768
- }
9769
- __sw_structAssertMemberGroupPresent(cols, decided.scope.member, 'insert');
9808
+ if (decided.scope.mode === 'member') __sw_structAssertMemberGroupPresent(cols, decided.scope.member, 'insert');
9809
+ // An upsert under a rule needs create (checked above) and, for "update", update on the
9810
+ // conflicting row; a rule that never grants update refuses before anything runs.
9811
+ if (onConflict === 'update' && decided.scope.mode === 'policy' && !__sw_structPolicyAllows(decided.scope.policy, 'update')) {
9812
+ __sw_structThrow403('POLICY_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and the declared private policy does not grant update. Use onConflict: "ignore", or grant update.');
9813
+ }
9814
+ if (onConflict === 'update' && decided.scope.mode === 'member' && !__sw_structMemberAllows(decided.scope.member, 'update')) {
9815
+ __sw_structThrow403('MEMBER_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and member() does not grant update. Use onConflict: "ignore", or grant update.');
9770
9816
  }
9771
9817
  if (cols.length === 0 && decided.scope.mode !== 'user'
9772
9818
  && !(decided.scope.mode === 'policy' && __sw_structPolicyHasOwner(decided.scope.policy))) __sw_structThrow('sw.db.insert requires at least one column value.');