@columnus/shared 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/dist/apiProfile.d.ts +1072 -0
  3. package/dist/apiProfile.js +4210 -0
  4. package/dist/apiProfile.js.map +1 -0
  5. package/dist/apiProfileSlug.d.ts +11 -0
  6. package/dist/apiProfileSlug.js +39 -0
  7. package/dist/apiProfileSlug.js.map +1 -0
  8. package/dist/apiToken.d.ts +6 -0
  9. package/dist/apiToken.js +35 -0
  10. package/dist/apiToken.js.map +1 -0
  11. package/dist/betterAuthSession.d.ts +8 -0
  12. package/dist/betterAuthSession.js +57 -0
  13. package/dist/betterAuthSession.js.map +1 -0
  14. package/dist/bffHost.d.ts +36 -0
  15. package/dist/bffHost.js +74 -0
  16. package/dist/bffHost.js.map +1 -0
  17. package/dist/bffProtocol.d.ts +1357 -0
  18. package/dist/bffProtocol.js +4362 -0
  19. package/dist/bffProtocol.js.map +1 -0
  20. package/dist/bffRedact.d.ts +30 -0
  21. package/dist/bffRedact.js +55 -0
  22. package/dist/bffRedact.js.map +1 -0
  23. package/dist/ciPreviewEnv.d.ts +49 -0
  24. package/dist/ciPreviewEnv.js +143 -0
  25. package/dist/ciPreviewEnv.js.map +1 -0
  26. package/dist/clipboardTransfer.d.ts +25 -0
  27. package/dist/clipboardTransfer.js +4862 -0
  28. package/dist/clipboardTransfer.js.map +1 -0
  29. package/dist/convexSiteUrl.d.ts +73 -0
  30. package/dist/convexSiteUrl.js +137 -0
  31. package/dist/convexSiteUrl.js.map +1 -0
  32. package/dist/csv.d.ts +54 -0
  33. package/dist/csv.js +5499 -0
  34. package/dist/csv.js.map +1 -0
  35. package/dist/dataTransfer.d.ts +21 -0
  36. package/dist/dataTransfer.js +74 -0
  37. package/dist/dataTransfer.js.map +1 -0
  38. package/dist/dateValues.d.ts +18 -0
  39. package/dist/dateValues.js +73 -0
  40. package/dist/dateValues.js.map +1 -0
  41. package/dist/deployOutput.d.ts +3 -0
  42. package/dist/deployOutput.js +23 -0
  43. package/dist/deployOutput.js.map +1 -0
  44. package/dist/displayLabel.d.ts +13 -0
  45. package/dist/displayLabel.js +4342 -0
  46. package/dist/displayLabel.js.map +1 -0
  47. package/dist/displayTemplateGenerator.d.ts +11 -0
  48. package/dist/displayTemplateGenerator.js +98 -0
  49. package/dist/displayTemplateGenerator.js.map +1 -0
  50. package/dist/displayTypes.d.ts +10 -0
  51. package/dist/displayTypes.js +3 -0
  52. package/dist/displayTypes.js.map +1 -0
  53. package/dist/dotenv.d.ts +8 -0
  54. package/dist/dotenv.js +61 -0
  55. package/dist/dotenv.js.map +1 -0
  56. package/dist/erdLayout.d.ts +27 -0
  57. package/dist/erdLayout.js +78 -0
  58. package/dist/erdLayout.js.map +1 -0
  59. package/dist/fieldTypeFormat.d.ts +5 -0
  60. package/dist/fieldTypeFormat.js +63 -0
  61. package/dist/fieldTypeFormat.js.map +1 -0
  62. package/dist/fieldTypeMeta.d.ts +50 -0
  63. package/dist/fieldTypeMeta.js +84 -0
  64. package/dist/fieldTypeMeta.js.map +1 -0
  65. package/dist/fieldValueEqual.d.ts +4 -0
  66. package/dist/fieldValueEqual.js +58 -0
  67. package/dist/fieldValueEqual.js.map +1 -0
  68. package/dist/importMatch.d.ts +80 -0
  69. package/dist/importMatch.js +5643 -0
  70. package/dist/importMatch.js.map +1 -0
  71. package/dist/index.d.ts +58 -0
  72. package/dist/index.js +10647 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/inferIdentifier.d.ts +18 -0
  75. package/dist/inferIdentifier.js +49 -0
  76. package/dist/inferIdentifier.js.map +1 -0
  77. package/dist/keyboard.d.ts +16 -0
  78. package/dist/keyboard.js +71 -0
  79. package/dist/keyboard.js.map +1 -0
  80. package/dist/manifestDeploy.d.ts +27 -0
  81. package/dist/manifestDeploy.js +16 -0
  82. package/dist/manifestDeploy.js.map +1 -0
  83. package/dist/manifestHash.d.ts +11 -0
  84. package/dist/manifestHash.js +35 -0
  85. package/dist/manifestHash.js.map +1 -0
  86. package/dist/previewDeployName.d.ts +26 -0
  87. package/dist/previewDeployName.js +77 -0
  88. package/dist/previewDeployName.js.map +1 -0
  89. package/dist/proposePolicy.d.ts +17 -0
  90. package/dist/proposePolicy.js +33 -0
  91. package/dist/proposePolicy.js.map +1 -0
  92. package/dist/queryBetween.d.ts +20 -0
  93. package/dist/queryBetween.js +28 -0
  94. package/dist/queryBetween.js.map +1 -0
  95. package/dist/queryExecution.d.ts +5 -0
  96. package/dist/queryExecution.js +58 -0
  97. package/dist/queryExecution.js.map +1 -0
  98. package/dist/queryFieldOps.d.ts +66 -0
  99. package/dist/queryFieldOps.js +138 -0
  100. package/dist/queryFieldOps.js.map +1 -0
  101. package/dist/queryMerge.d.ts +29 -0
  102. package/dist/queryMerge.js +4277 -0
  103. package/dist/queryMerge.js.map +1 -0
  104. package/dist/queryPlan.d.ts +97 -0
  105. package/dist/queryPlan.js +4564 -0
  106. package/dist/queryPlan.js.map +1 -0
  107. package/dist/queryStudio.d.ts +25 -0
  108. package/dist/queryStudio.js +4658 -0
  109. package/dist/queryStudio.js.map +1 -0
  110. package/dist/queryTypes.d.ts +402 -0
  111. package/dist/queryTypes.js +4239 -0
  112. package/dist/queryTypes.js.map +1 -0
  113. package/dist/queryValidators.d.ts +99 -0
  114. package/dist/queryValidators.js +76 -0
  115. package/dist/queryValidators.js.map +1 -0
  116. package/dist/readApiQuery.d.ts +10 -0
  117. package/dist/readApiQuery.js +24 -0
  118. package/dist/readApiQuery.js.map +1 -0
  119. package/dist/recordTableColumns.d.ts +21 -0
  120. package/dist/recordTableColumns.js +4469 -0
  121. package/dist/recordTableColumns.js.map +1 -0
  122. package/dist/refDisplayRequests.d.ts +17 -0
  123. package/dist/refDisplayRequests.js +4877 -0
  124. package/dist/refDisplayRequests.js.map +1 -0
  125. package/dist/refGraph.d.ts +23 -0
  126. package/dist/refGraph.js +102 -0
  127. package/dist/refGraph.js.map +1 -0
  128. package/dist/refsRecompute.d.ts +8 -0
  129. package/dist/refsRecompute.js +12 -0
  130. package/dist/refsRecompute.js.map +1 -0
  131. package/dist/richDoc.d.ts +67 -0
  132. package/dist/richDoc.js +4408 -0
  133. package/dist/richDoc.js.map +1 -0
  134. package/dist/schemaGenerator.d.ts +31 -0
  135. package/dist/schemaGenerator.js +198 -0
  136. package/dist/schemaGenerator.js.map +1 -0
  137. package/dist/schemaShrink.d.ts +74 -0
  138. package/dist/schemaShrink.js +132 -0
  139. package/dist/schemaShrink.js.map +1 -0
  140. package/dist/schemaShrinkValidators.d.ts +72 -0
  141. package/dist/schemaShrinkValidators.js +31 -0
  142. package/dist/schemaShrinkValidators.js.map +1 -0
  143. package/dist/schemaTypes.d.ts +454 -0
  144. package/dist/schemaTypes.js +4557 -0
  145. package/dist/schemaTypes.js.map +1 -0
  146. package/dist/share.d.ts +8 -0
  147. package/dist/share.js +4755 -0
  148. package/dist/share.js.map +1 -0
  149. package/dist/shareApiBridge.d.ts +18 -0
  150. package/dist/shareApiBridge.js +4283 -0
  151. package/dist/shareApiBridge.js.map +1 -0
  152. package/dist/shareGuestPolicy.d.ts +14 -0
  153. package/dist/shareGuestPolicy.js +90 -0
  154. package/dist/shareGuestPolicy.js.map +1 -0
  155. package/dist/shareProposal.d.ts +113 -0
  156. package/dist/shareProposal.js +4810 -0
  157. package/dist/shareProposal.js.map +1 -0
  158. package/dist/shareProposalDiff.d.ts +13 -0
  159. package/dist/shareProposalDiff.js +91 -0
  160. package/dist/shareProposalDiff.js.map +1 -0
  161. package/dist/shareScopes.d.ts +14 -0
  162. package/dist/shareScopes.js +223 -0
  163. package/dist/shareScopes.js.map +1 -0
  164. package/dist/shareTokenCrypto.d.ts +9 -0
  165. package/dist/shareTokenCrypto.js +87 -0
  166. package/dist/shareTokenCrypto.js.map +1 -0
  167. package/dist/shareTokens.d.ts +6 -0
  168. package/dist/shareTokens.js +26 -0
  169. package/dist/shareTokens.js.map +1 -0
  170. package/dist/shareTypes.d.ts +107 -0
  171. package/dist/shareTypes.js +3 -0
  172. package/dist/shareTypes.js.map +1 -0
  173. package/dist/shareValidators.d.ts +168 -0
  174. package/dist/shareValidators.js +124 -0
  175. package/dist/shareValidators.js.map +1 -0
  176. package/dist/shareWizard.d.ts +14 -0
  177. package/dist/shareWizard.js +200 -0
  178. package/dist/shareWizard.js.map +1 -0
  179. package/dist/spaceBundle.d.ts +67 -0
  180. package/dist/spaceBundle.js +4664 -0
  181. package/dist/spaceBundle.js.map +1 -0
  182. package/dist/storageFieldValues.d.ts +10 -0
  183. package/dist/storageFieldValues.js +4421 -0
  184. package/dist/storageFieldValues.js.map +1 -0
  185. package/dist/studioBoot.d.ts +49 -0
  186. package/dist/studioBoot.js +167 -0
  187. package/dist/studioBoot.js.map +1 -0
  188. package/dist/syncClampMembership.d.ts +22 -0
  189. package/dist/syncClampMembership.js +74 -0
  190. package/dist/syncClampMembership.js.map +1 -0
  191. package/dist/tenantHosts.d.ts +30 -0
  192. package/dist/tenantHosts.js +60 -0
  193. package/dist/tenantHosts.js.map +1 -0
  194. package/dist/tenantSecretsCrypto.d.ts +31 -0
  195. package/dist/tenantSecretsCrypto.js +145 -0
  196. package/dist/tenantSecretsCrypto.js.map +1 -0
  197. package/dist/useDebouncedCommit.d.ts +11 -0
  198. package/dist/useDebouncedCommit.js +34 -0
  199. package/dist/useDebouncedCommit.js.map +1 -0
  200. package/dist/useDebouncedValue.d.ts +1 -0
  201. package/dist/useDebouncedValue.js +15 -0
  202. package/dist/useDebouncedValue.js.map +1 -0
  203. package/dist/useModifierHeld.d.ts +2 -0
  204. package/dist/useModifierHeld.js +28 -0
  205. package/dist/useModifierHeld.js.map +1 -0
  206. package/package.json +36 -0
@@ -0,0 +1,49 @@
1
+ export type StudioBootEnv = {
2
+ host: string;
3
+ managementConvexUrl?: string;
4
+ viteConvexUrl?: string;
5
+ viteConvexSiteUrl?: string;
6
+ };
7
+ export type StudioBootPlan = {
8
+ mode: "management";
9
+ host: string;
10
+ slug: string;
11
+ managementConvexUrl: string;
12
+ } | {
13
+ mode: "baked";
14
+ convexUrl: string;
15
+ convexSiteUrl: string;
16
+ reason: "preview" | "no-tenant-host" | "no-management-url";
17
+ } | {
18
+ mode: "error";
19
+ code: "missing_config" | "not_a_tenant_host";
20
+ message: string;
21
+ };
22
+ /** Preview Studio hosts (`preview-*.columnus.app`). */
23
+ export declare function isPreviewStudioHost(host: string): boolean;
24
+ /**
25
+ * Decide how Studio should obtain its tenant Convex URL.
26
+ * Does not call the network — management mode still needs resolveTenantByHost.
27
+ */
28
+ export declare function planStudioBoot(env: StudioBootEnv): StudioBootPlan;
29
+ export type StudioHardGateReason = "unknown" | "disabled" | "resolve_failed" | "config";
30
+ export type StudioBootResult = {
31
+ ok: true;
32
+ convexUrl: string;
33
+ convexSiteUrl: string;
34
+ source: "management" | "baked";
35
+ slug?: string;
36
+ displayName?: string;
37
+ } | {
38
+ ok: false;
39
+ reason: StudioHardGateReason;
40
+ message: string;
41
+ slug?: string;
42
+ displayName?: string;
43
+ };
44
+ /**
45
+ * Guest share URLs (`/s/{token}`) must open the Convex WebSocket without a JWT.
46
+ * `expectAuth: true` pauses the socket until `setAuth`, which never runs for
47
+ * anonymous recipients — they stay stuck on "Opening secure link…".
48
+ */
49
+ export declare function shouldExpectConvexAuth(pathname: string): boolean;
@@ -0,0 +1,167 @@
1
+ // packages/@columnus/shared/src/tenantHosts.ts
2
+ var RESERVED_TENANT_SLUGS = ["manage", "deploy", "www"];
3
+ var RESERVED_SLUG_SET = new Set(RESERVED_TENANT_SLUGS);
4
+ var TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ function isReservedTenantSlug(slug) {
6
+ const normalized = slug.trim().toLowerCase();
7
+ if (!normalized) return true;
8
+ if (RESERVED_SLUG_SET.has(normalized)) return true;
9
+ if (normalized.startsWith("preview-")) return true;
10
+ return false;
11
+ }
12
+ function isValidTenantSlug(slug) {
13
+ const trimmed = slug.trim();
14
+ if (!TENANT_SLUG_RE.test(trimmed)) return false;
15
+ return !isReservedTenantSlug(trimmed);
16
+ }
17
+ function hostnameFromHostHeader(host) {
18
+ const trimmed = host.trim().toLowerCase();
19
+ if (!trimmed) return "";
20
+ if (trimmed.startsWith("[")) {
21
+ const end = trimmed.indexOf("]");
22
+ return end === -1 ? trimmed : trimmed.slice(1, end);
23
+ }
24
+ const colon = trimmed.lastIndexOf(":");
25
+ if (colon > -1 && /^\d+$/.test(trimmed.slice(colon + 1))) {
26
+ return trimmed.slice(0, colon);
27
+ }
28
+ return trimmed;
29
+ }
30
+ function tenantSlugFromHost(host) {
31
+ const hostname = hostnameFromHostHeader(host);
32
+ const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|localhost)$/);
33
+ if (!match) return null;
34
+ const slug = match[1];
35
+ if (!slug || !isValidTenantSlug(slug)) return null;
36
+ return slug;
37
+ }
38
+ var PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;
39
+ function isValidPreviewTenantSlug(slug) {
40
+ return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());
41
+ }
42
+ function previewSlugFromHost(host) {
43
+ const hostname = hostnameFromHostHeader(host);
44
+ const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|localhost)$/);
45
+ if (!match) return null;
46
+ const slug = match[1];
47
+ if (!slug || !isValidPreviewTenantSlug(slug)) return null;
48
+ return slug;
49
+ }
50
+ function anyTenantSlugFromHost(host) {
51
+ return tenantSlugFromHost(host) ?? previewSlugFromHost(host);
52
+ }
53
+
54
+ // packages/@columnus/shared/src/convexSiteUrl.ts
55
+ function convexCloudToSiteUrl(convexUrl) {
56
+ const trimmed = convexUrl.trim().replace(/\/$/, "");
57
+ if (trimmed.includes(".convex.site")) return trimmed;
58
+ if (trimmed.includes(".convex.cloud")) {
59
+ return trimmed.replace(/\.convex\.cloud\b/, ".convex.site");
60
+ }
61
+ return trimmed;
62
+ }
63
+ function trimOptional(value) {
64
+ const trimmed = value?.trim();
65
+ return trimmed || void 0;
66
+ }
67
+ function normalizeTrailingSlash(url) {
68
+ return url.trim().replace(/\/$/, "");
69
+ }
70
+ function deriveSiteFromCloudUrl(cloud) {
71
+ const trimmed = cloud?.trim();
72
+ if (!trimmed) return void 0;
73
+ if (trimmed.includes(".convex.cloud") || trimmed.includes(".convex.site")) {
74
+ return convexCloudToSiteUrl(trimmed);
75
+ }
76
+ return void 0;
77
+ }
78
+ function convexDeploymentSlug(url) {
79
+ const match = url.match(/https:\/\/([^.]+)\.convex\.(?:cloud|site)/);
80
+ return match?.[1];
81
+ }
82
+ function explicitSiteMatchesCloud(explicit, cloud) {
83
+ const explicitSlug = convexDeploymentSlug(explicit);
84
+ const cloudSlug = convexDeploymentSlug(cloud);
85
+ if (!explicitSlug || !cloudSlug) return true;
86
+ return explicitSlug === cloudSlug;
87
+ }
88
+ function resolveConvexSiteUrlFromEnv(env) {
89
+ const explicit = trimOptional(env.convexSiteUrl) ?? trimOptional(env.viteConvexSiteUrl);
90
+ const cloud = trimOptional(env.convexUrl) ?? trimOptional(env.viteConvexUrl) ?? trimOptional(env.convexCloudUrl);
91
+ if (explicit && cloud && !explicitSiteMatchesCloud(explicit, cloud)) {
92
+ return deriveSiteFromCloudUrl(cloud) ?? normalizeTrailingSlash(explicit);
93
+ }
94
+ if (explicit) return normalizeTrailingSlash(explicit);
95
+ return deriveSiteFromCloudUrl(cloud) ?? "";
96
+ }
97
+
98
+ // packages/@columnus/shared/src/studioBoot.ts
99
+ function isPreviewStudioHost(host) {
100
+ const hostname = hostnameFromHostHeader(host);
101
+ return /^preview-[a-z0-9-]+\.columnus\.app$/i.test(hostname);
102
+ }
103
+ function normalizeUrl(url) {
104
+ return url.trim().replace(/\/+$/, "");
105
+ }
106
+ function siteUrlFromCloudOrExplicit(cloudUrl, explicitSite) {
107
+ return resolveConvexSiteUrlFromEnv({
108
+ convexUrl: cloudUrl,
109
+ convexSiteUrl: explicitSite
110
+ });
111
+ }
112
+ function planStudioBoot(env) {
113
+ const management = env.managementConvexUrl?.trim();
114
+ const baked = env.viteConvexUrl?.trim();
115
+ const host = env.host;
116
+ if (management) {
117
+ const slug = anyTenantSlugFromHost(host);
118
+ if (slug) {
119
+ return {
120
+ mode: "management",
121
+ host: hostnameFromHostHeader(host),
122
+ slug,
123
+ managementConvexUrl: normalizeUrl(management)
124
+ };
125
+ }
126
+ return {
127
+ mode: "error",
128
+ code: "not_a_tenant_host",
129
+ message: "This host is not a tenant subdomain (expected {slug}.columnus.app or {slug}.columnus.localhost). Open a registered tenant host such as https://app.columnus.localhost."
130
+ };
131
+ }
132
+ if (isPreviewStudioHost(host)) {
133
+ if (!baked) {
134
+ return {
135
+ mode: "error",
136
+ code: "missing_config",
137
+ message: "Preview hosts require VITE_MANAGEMENT_CONVEX_URL (Manage resolve) or VITE_CONVEX_URL (legacy bake)."
138
+ };
139
+ }
140
+ return {
141
+ mode: "baked",
142
+ convexUrl: normalizeUrl(baked),
143
+ convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),
144
+ reason: "preview"
145
+ };
146
+ }
147
+ if (baked) {
148
+ return {
149
+ mode: "baked",
150
+ convexUrl: normalizeUrl(baked),
151
+ convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),
152
+ reason: "no-management-url"
153
+ };
154
+ }
155
+ return {
156
+ mode: "error",
157
+ code: "missing_config",
158
+ message: "Set VITE_MANAGEMENT_CONVEX_URL (multi-tenant) or VITE_CONVEX_URL (single deployment)."
159
+ };
160
+ }
161
+ function shouldExpectConvexAuth(pathname) {
162
+ return !pathname.startsWith("/s/");
163
+ }
164
+
165
+ export { isPreviewStudioHost, planStudioBoot, shouldExpectConvexAuth };
166
+ //# sourceMappingURL=studioBoot.js.map
167
+ //# sourceMappingURL=studioBoot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tenantHosts.ts","../src/convexSiteUrl.ts","../src/studioBoot.ts"],"names":[],"mappings":"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","file":"studioBoot.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n return env === \"local\" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n","/** Convex deployment cloud URL (.convex.cloud) → HTTP actions / auth issuer (.convex.site). */\nexport function convexCloudToSiteUrl(convexUrl: string): string {\n const trimmed = convexUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.site\")) return trimmed;\n if (trimmed.includes(\".convex.cloud\")) {\n return trimmed.replace(/\\.convex\\.cloud\\b/, \".convex.site\");\n }\n return trimmed;\n}\n\nexport function convexSiteToCloudUrl(siteUrl: string): string {\n const trimmed = siteUrl.trim().replace(/\\/$/, \"\");\n if (trimmed.includes(\".convex.cloud\")) return trimmed;\n if (trimmed.includes(\".convex.site\")) {\n return trimmed.replace(/\\.convex\\.site\\b/, \".convex.cloud\");\n }\n return trimmed;\n}\n\ntype ProcessEnvLike = Record<string, string | undefined>;\n\nfunction trimOptional(value?: string): string | undefined {\n const trimmed = value?.trim();\n return trimmed || undefined;\n}\n\nfunction normalizeTrailingSlash(url: string): string {\n return url.trim().replace(/\\/$/, \"\");\n}\n\nfunction deriveSiteFromCloudUrl(cloud?: string): string | undefined {\n const trimmed = cloud?.trim();\n if (!trimmed) return undefined;\n if (trimmed.includes(\".convex.cloud\") || trimmed.includes(\".convex.site\")) {\n return convexCloudToSiteUrl(trimmed);\n }\n return undefined;\n}\n\n/** Docker runtime-base verify placeholder — must not be used for real BFF prepare. */\nexport function isPlaceholderManagementSiteUrl(url: string): boolean {\n try {\n const host = new URL(/:\\/\\//.test(url) ? url : `https://${url}`).hostname;\n return host === \"placeholder.convex.site\" || host.endsWith(\".placeholder.convex.site\");\n } catch {\n return /placeholder\\.convex\\.site/i.test(url);\n }\n}\n\n/**\n * Management `.convex.site` origin for BFF `prepare`.\n * Prefers explicit management site env vars, else derives from `VITE_MANAGEMENT_CONVEX_URL`.\n * Does not read `VITE_CONVEX_SITE_URL` — on web that is the tenant site, not Manage.\n */\nexport function resolveBffManagementSiteUrl(env: ProcessEnvLike = process.env): string | undefined {\n for (const key of [\"COLUMNUS_MANAGEMENT_SITE_URL\", \"MANAGEMENT_CONVEX_SITE_URL\"] as const) {\n const value = trimOptional(env[key]);\n if (value && !isPlaceholderManagementSiteUrl(value)) {\n return normalizeTrailingSlash(value);\n }\n }\n const managementCloud = trimOptional(env.VITE_MANAGEMENT_CONVEX_URL);\n if (managementCloud) return convexCloudToSiteUrl(managementCloud);\n return undefined;\n}\n\n/**\n * Runtime BFF management site: prefer image-baked URL over Coolify env.\n * Preview Coolify often injects prod `MANAGEMENT_CONVEX_SITE_URL`, which shadows the\n * ephemeral Manage preview bake and makes prepare return HTTP 404 (no routes on prod).\n */\nexport function resolveRuntimeManagementSiteUrl(options: {\n bakedSiteUrl?: string | null;\n env?: ProcessEnvLike;\n}): string | undefined {\n const bakedSite = trimOptional(options.bakedSiteUrl ?? undefined);\n if (bakedSite && !isPlaceholderManagementSiteUrl(bakedSite)) {\n return normalizeTrailingSlash(bakedSite);\n }\n return resolveBffManagementSiteUrl(options.env ?? process.env);\n}\n\nfunction convexDeploymentSlug(url: string): string | undefined {\n const match = url.match(/https:\\/\\/([^.]+)\\.convex\\.(?:cloud|site)/);\n return match?.[1];\n}\n\n/** Deployment name from a Convex cloud/site URL (`https://{name}.convex.cloud`). */\nexport function convexDeploymentSlugFromUrl(url: string): string | undefined {\n return convexDeploymentSlug(url);\n}\n\n/**\n * Best-effort `CONVEX_DEPLOYMENT` when only the cloud URL is known (e.g. after `columnus-api pull`).\n * Defaults to `dev:{slug}` — preview/prod deployments may need an explicit value in `.env.local`.\n */\nexport function deriveConvexDeploymentFromCloudUrl(cloudUrl: string): string | undefined {\n const slug = convexDeploymentSlug(cloudUrl);\n return slug ? `dev:${slug}` : undefined;\n}\n\nfunction explicitSiteMatchesCloud(explicit: string, cloud: string): boolean {\n const explicitSlug = convexDeploymentSlug(explicit);\n const cloudSlug = convexDeploymentSlug(cloud);\n if (!explicitSlug || !cloudSlug) return true;\n return explicitSlug === cloudSlug;\n}\n\nexport type ConvexSiteUrlEnv = {\n /** CONVEX_SITE_URL */\n convexSiteUrl?: string;\n /** VITE_CONVEX_SITE_URL */\n viteConvexSiteUrl?: string;\n /** CONVEX_URL */\n convexUrl?: string;\n /** VITE_CONVEX_URL */\n viteConvexUrl?: string;\n /** CONVEX_CLOUD_URL */\n convexCloudUrl?: string;\n};\n\n/** @deprecated Use `ConvexSiteUrlEnv` */\nexport type ResolveConvexSiteUrlOptions = ConvexSiteUrlEnv;\n\nexport function readConvexSiteUrlEnv(env: ProcessEnvLike): ConvexSiteUrlEnv {\n return {\n convexSiteUrl: env.CONVEX_SITE_URL,\n viteConvexSiteUrl: env.VITE_CONVEX_SITE_URL,\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n convexCloudUrl: env.CONVEX_CLOUD_URL,\n };\n}\n\n/**\n * Resolve .convex.site URL from env: explicit site URLs first, then derive from Convex cloud/site URLs.\n * Returns empty string when nothing is configured.\n */\nexport function resolveConvexSiteUrlFromEnv(env: ConvexSiteUrlEnv): string {\n const explicit = trimOptional(env.convexSiteUrl) ?? trimOptional(env.viteConvexSiteUrl);\n const cloud =\n trimOptional(env.convexUrl) ??\n trimOptional(env.viteConvexUrl) ??\n trimOptional(env.convexCloudUrl);\n\n if (explicit && cloud && !explicitSiteMatchesCloud(explicit, cloud)) {\n return deriveSiteFromCloudUrl(cloud) ?? normalizeTrailingSlash(explicit);\n }\n\n if (explicit) return normalizeTrailingSlash(explicit);\n\n return deriveSiteFromCloudUrl(cloud) ?? \"\";\n}\n\n/** Better Auth `baseURL` from standard Convex process env. */\nexport function convexSiteUrlFromProcessEnv(env: ProcessEnvLike = process.env): string {\n return resolveConvexSiteUrlFromEnv(readConvexSiteUrlEnv(env));\n}\n\n/** Resolve .convex.site URL for OTP sign-in and scripts (throws when unset). */\nexport function resolveConvexSiteUrl(env?: ConvexSiteUrlEnv): string {\n const url = resolveConvexSiteUrlFromEnv(env ?? {});\n if (!url) {\n throw new Error(\"Convex URL required to derive .convex.site URL for OTP sign-in\");\n }\n return url;\n}\n\nexport type AuthConfigIssuerEnv = ConvexSiteUrlEnv & {\n /** BAKED_AUTH_ISSUER patched at deploy time */\n bakedAuthIssuer?: string;\n};\n\n/**\n * Auth issuer for deploy scripts: baked deploy patch → convex site URL env chain.\n */\nexport function resolveAuthConfigIssuerFromEnv(env: AuthConfigIssuerEnv): string {\n const baked = trimOptional(env.bakedAuthIssuer);\n if (baked) return normalizeTrailingSlash(baked);\n\n return resolveConvexSiteUrlFromEnv(env);\n}\n\nexport type AuthConfigIssuerFromProcessEnvOptions = {\n bakedAuthIssuer?: string;\n /** Derive from CONVEX_URL / VITE_CONVEX_URL when issuer env vars are unset (deploy scripts). */\n deriveFromCloudUrl?: boolean;\n};\n\n/** JWT issuer from process env (auth.config.ts bundle vs deploy scripts). */\nexport function authConfigIssuerFromProcessEnv(\n env: ProcessEnvLike = process.env,\n options: AuthConfigIssuerFromProcessEnvOptions = {},\n): string {\n return resolveAuthConfigIssuerFromEnv({\n bakedAuthIssuer: options.bakedAuthIssuer,\n convexSiteUrl: env.CONVEX_SITE_URL,\n ...(options.deriveFromCloudUrl\n ? {\n convexUrl: env.CONVEX_URL,\n viteConvexUrl: env.VITE_CONVEX_URL,\n }\n : {}),\n });\n}\n\n/** Comma-separated browser origins (SITE_URL) for Better Auth trustedOrigins / crossDomain. */\nexport function parseTrustedSiteUrls(siteUrlRaw?: string): string[] {\n const raw = siteUrlRaw?.trim() ?? \"\";\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((url) => url.trim())\n .filter(Boolean);\n}\n\nexport function primaryTrustedSiteUrl(siteUrlRaw?: string): string {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n return urls[0] ?? siteUrlRaw?.trim() ?? \"\";\n}\n\n/** Better Auth `trustedOrigins` from SITE_URL. */\nexport function betterAuthTrustedOrigins(siteUrlRaw?: string): string[] {\n const urls = parseTrustedSiteUrls(siteUrlRaw);\n if (urls.length > 0) return urls;\n const primary = primaryTrustedSiteUrl(siteUrlRaw);\n return primary ? [primary] : [];\n}\n","import { anyTenantSlugFromHost, hostnameFromHostHeader } from \"./tenantHosts.ts\";\nimport { resolveConvexSiteUrlFromEnv } from \"./convexSiteUrl.ts\";\n\nexport type StudioBootEnv = {\n host: string;\n managementConvexUrl?: string;\n viteConvexUrl?: string;\n viteConvexSiteUrl?: string;\n};\n\nexport type StudioBootPlan =\n | { mode: \"management\"; host: string; slug: string; managementConvexUrl: string }\n | {\n mode: \"baked\";\n convexUrl: string;\n convexSiteUrl: string;\n reason: \"preview\" | \"no-tenant-host\" | \"no-management-url\";\n }\n | { mode: \"error\"; code: \"missing_config\" | \"not_a_tenant_host\"; message: string };\n\n/** Preview Studio hosts (`preview-*.columnus.app`). */\nexport function isPreviewStudioHost(host: string): boolean {\n const hostname = hostnameFromHostHeader(host);\n return /^preview-[a-z0-9-]+\\.columnus\\.app$/i.test(hostname);\n}\n\nfunction normalizeUrl(url: string): string {\n return url.trim().replace(/\\/+$/, \"\");\n}\n\nfunction siteUrlFromCloudOrExplicit(cloudUrl: string, explicitSite?: string): string {\n return resolveConvexSiteUrlFromEnv({\n convexUrl: cloudUrl,\n convexSiteUrl: explicitSite,\n });\n}\n\n/**\n * Decide how Studio should obtain its tenant Convex URL.\n * Does not call the network — management mode still needs resolveTenantByHost.\n */\nexport function planStudioBoot(env: StudioBootEnv): StudioBootPlan {\n const management = env.managementConvexUrl?.trim();\n const baked = env.viteConvexUrl?.trim();\n const host = env.host;\n\n if (management) {\n const slug = anyTenantSlugFromHost(host);\n if (slug) {\n return {\n mode: \"management\",\n host: hostnameFromHostHeader(host),\n slug,\n managementConvexUrl: normalizeUrl(management),\n };\n }\n // Multi-tenant default: hard-gate apex / loopback / unknown hosts (D49).\n return {\n mode: \"error\",\n code: \"not_a_tenant_host\",\n message:\n \"This host is not a tenant subdomain (expected {slug}.columnus.app or {slug}.columnus.localhost). Open a registered tenant host such as https://app.columnus.localhost.\",\n };\n }\n\n // A baked single-deployment host does not need Management resolution.\n if (isPreviewStudioHost(host)) {\n if (!baked) {\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Preview hosts require VITE_MANAGEMENT_CONVEX_URL (Manage resolve) or VITE_CONVEX_URL (legacy bake).\",\n };\n }\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"preview\",\n };\n }\n\n if (baked) {\n return {\n mode: \"baked\",\n convexUrl: normalizeUrl(baked),\n convexSiteUrl: siteUrlFromCloudOrExplicit(normalizeUrl(baked), env.viteConvexSiteUrl),\n reason: \"no-management-url\",\n };\n }\n\n return {\n mode: \"error\",\n code: \"missing_config\",\n message:\n \"Set VITE_MANAGEMENT_CONVEX_URL (multi-tenant) or VITE_CONVEX_URL (single deployment).\",\n };\n}\n\nexport type StudioHardGateReason = \"unknown\" | \"disabled\" | \"resolve_failed\" | \"config\";\n\nexport type StudioBootResult =\n | {\n ok: true;\n convexUrl: string;\n convexSiteUrl: string;\n source: \"management\" | \"baked\";\n slug?: string;\n displayName?: string;\n }\n | {\n ok: false;\n reason: StudioHardGateReason;\n message: string;\n slug?: string;\n displayName?: string;\n };\n\n/**\n * Guest share URLs (`/s/{token}`) must open the Convex WebSocket without a JWT.\n * `expectAuth: true` pauses the socket until `setAuth`, which never runs for\n * anonymous recipients — they stay stuck on \"Opening secure link…\".\n */\nexport function shouldExpectConvexAuth(pathname: string): boolean {\n return !pathname.startsWith(\"/s/\");\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { ApiTableAccess } from "./apiProfile.ts";
2
+ import type { Predicate } from "./queryTypes.ts";
3
+ /** Sync change after host membership resolution (null = suppress for this profile). */
4
+ export type SyncMembershipOp = "upsert" | "delete";
5
+ export type SyncRowSnapshot = Record<string, unknown> | null | undefined;
6
+ /**
7
+ * Clamp predicates that gate webhook Sync membership for a table.
8
+ * Empty / missing → unrestricted (all rows in the syncing table).
9
+ * Sync ignores clamp `order` / `search` (Read/list defaults only).
10
+ */
11
+ export declare function syncClampPredicates(tableAccess: ApiTableAccess[] | undefined, table: string): Predicate[];
12
+ export declare function documentMatchesSyncClamp(doc: Record<string, unknown> | null | undefined, predicates: Predicate[]): boolean;
13
+ /**
14
+ * Resolve the outbox op for one profile after a host row mutation.
15
+ * See plan 046 grill lock (leave → delete; hard-delete outside clamp → suppress).
16
+ */
17
+ export declare function resolveSyncMembershipOp(args: {
18
+ predicates: Predicate[];
19
+ before: SyncRowSnapshot;
20
+ after: SyncRowSnapshot;
21
+ requestedOp: SyncMembershipOp;
22
+ }): SyncMembershipOp | null;
@@ -0,0 +1,74 @@
1
+ // packages/@columnus/shared/src/queryBetween.ts
2
+ function matchesBetween(value, between) {
3
+ if (between.min != null && compareOrdered(value, between.min) < 0) return false;
4
+ if (between.max != null && compareOrdered(value, between.max) > 0) return false;
5
+ return true;
6
+ }
7
+ function compareOrdered(a, b) {
8
+ if (typeof a === "number" && typeof b === "number") return a - b;
9
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0;
10
+ if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
11
+ if (a === b) return 0;
12
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
13
+ }
14
+
15
+ // packages/@columnus/shared/src/queryExecution.ts
16
+ function documentMatchesPredicates(doc, predicates) {
17
+ for (const predicate of predicates) {
18
+ if (!documentMatchesPredicate(doc, predicate)) return false;
19
+ }
20
+ return true;
21
+ }
22
+ function documentMatchesPredicate(doc, predicate) {
23
+ const value = doc[predicate.field];
24
+ switch (predicate.op) {
25
+ case "eq":
26
+ return value === predicate.value;
27
+ case "neq":
28
+ return value !== predicate.value;
29
+ case "in":
30
+ return predicate.value.some((candidate) => candidate === value);
31
+ case "gt":
32
+ return compareOrdered(value, predicate.value) > 0;
33
+ case "gte":
34
+ return compareOrdered(value, predicate.value) >= 0;
35
+ case "lt":
36
+ return compareOrdered(value, predicate.value) < 0;
37
+ case "lte":
38
+ return compareOrdered(value, predicate.value) <= 0;
39
+ case "between":
40
+ return matchesBetween(value, predicate.value);
41
+ case "containsAll":
42
+ return Array.isArray(value) && predicate.value.every((candidate) => value.includes(candidate));
43
+ default: {
44
+ const exhaustive = predicate;
45
+ return exhaustive;
46
+ }
47
+ }
48
+ }
49
+
50
+ // packages/@columnus/shared/src/syncClampMembership.ts
51
+ function syncClampPredicates(tableAccess, table) {
52
+ const access = tableAccess?.find((candidate) => candidate.table === table);
53
+ return access?.rowQuery?.predicates ?? [];
54
+ }
55
+ function documentMatchesSyncClamp(doc, predicates) {
56
+ if (!doc) return false;
57
+ return documentMatchesPredicates(doc, predicates);
58
+ }
59
+ function resolveSyncMembershipOp(args) {
60
+ const { predicates, before, after, requestedOp } = args;
61
+ if (predicates.length === 0) return requestedOp;
62
+ const beforeMatch = documentMatchesSyncClamp(before, predicates);
63
+ const afterMatch = documentMatchesSyncClamp(after, predicates);
64
+ if (requestedOp === "delete") {
65
+ return beforeMatch ? "delete" : null;
66
+ }
67
+ if (afterMatch) return "upsert";
68
+ if (beforeMatch) return "delete";
69
+ return null;
70
+ }
71
+
72
+ export { documentMatchesSyncClamp, resolveSyncMembershipOp, syncClampPredicates };
73
+ //# sourceMappingURL=syncClampMembership.js.map
74
+ //# sourceMappingURL=syncClampMembership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/queryBetween.ts","../src/queryExecution.ts","../src/syncClampMembership.ts"],"names":[],"mappings":"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","file":"syncClampMembership.js","sourcesContent":["export type BetweenValue = {\n min: unknown;\n max: unknown;\n};\n\nexport type BetweenIndexBound =\n | { op: \"gte\"; value: unknown }\n | { op: \"lte\"; value: unknown }\n | { op: \"between\"; min: unknown; max: unknown };\n\nexport function isCompleteBetween(value: BetweenValue): boolean {\n return value.min != null && value.max != null;\n}\n\n/** Convert inclusive `between` UI state into the strongest available index range. */\nexport function betweenToIndexBound(value: BetweenValue): BetweenIndexBound | null {\n if (isCompleteBetween(value)) {\n return { op: \"between\", min: value.min, max: value.max };\n }\n if (value.min != null) return { op: \"gte\", value: value.min };\n if (value.max != null) return { op: \"lte\", value: value.max };\n return null;\n}\n\nexport function matchesBetween(value: unknown, between: BetweenValue): boolean {\n if (between.min != null && compareOrdered(value, between.min) < 0) return false;\n if (between.max != null && compareOrdered(value, between.max) > 0) return false;\n return true;\n}\n\nexport function compareOrdered(a: unknown, b: unknown): number {\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"string\" && typeof b === \"string\") return a < b ? -1 : a > b ? 1 : 0;\n if (typeof a === \"bigint\" && typeof b === \"bigint\") return a < b ? -1 : a > b ? 1 : 0;\n if (a === b) return 0;\n return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;\n}\n","import { compareOrdered, matchesBetween } from \"./queryBetween.ts\";\nimport type { Predicate, QueryOrder } from \"./queryTypes.ts\";\n\n/** Evaluate residual predicates against a document. */\nexport function documentMatchesPredicates(\n doc: Record<string, unknown>,\n predicates: Predicate[],\n): boolean {\n for (const predicate of predicates) {\n if (!documentMatchesPredicate(doc, predicate)) return false;\n }\n return true;\n}\n\nexport function documentMatchesPredicate(\n doc: Record<string, unknown>,\n predicate: Predicate,\n): boolean {\n const value = doc[predicate.field];\n switch (predicate.op) {\n case \"eq\":\n return value === predicate.value;\n case \"neq\":\n return value !== predicate.value;\n case \"in\":\n return predicate.value.some((candidate) => candidate === value);\n case \"gt\":\n return compareOrdered(value, predicate.value) > 0;\n case \"gte\":\n return compareOrdered(value, predicate.value) >= 0;\n case \"lt\":\n return compareOrdered(value, predicate.value) < 0;\n case \"lte\":\n return compareOrdered(value, predicate.value) <= 0;\n case \"between\":\n return matchesBetween(value, predicate.value);\n case \"containsAll\":\n return (\n Array.isArray(value) && predicate.value.every((candidate) => value.includes(candidate))\n );\n default: {\n const exhaustive: never = predicate;\n return exhaustive;\n }\n }\n}\n\nexport function sortDocuments<T extends Record<string, unknown>>(\n docs: T[],\n order: QueryOrder,\n): T[] {\n const direction = order.direction === \"desc\" ? -1 : 1;\n return [...docs].sort(\n (left, right) => direction * compareOrdered(left[order.field], right[order.field]),\n );\n}\n","import type { ApiTableAccess } from \"./apiProfile.ts\";\nimport { documentMatchesPredicates } from \"./queryExecution.ts\";\nimport type { Predicate } from \"./queryTypes.ts\";\n\n/** Sync change after host membership resolution (null = suppress for this profile). */\nexport type SyncMembershipOp = \"upsert\" | \"delete\";\n\nexport type SyncRowSnapshot = Record<string, unknown> | null | undefined;\n\n/**\n * Clamp predicates that gate webhook Sync membership for a table.\n * Empty / missing → unrestricted (all rows in the syncing table).\n * Sync ignores clamp `order` / `search` (Read/list defaults only).\n */\nexport function syncClampPredicates(\n tableAccess: ApiTableAccess[] | undefined,\n table: string,\n): Predicate[] {\n const access = tableAccess?.find((candidate) => candidate.table === table);\n return access?.rowQuery?.predicates ?? [];\n}\n\nexport function documentMatchesSyncClamp(\n doc: Record<string, unknown> | null | undefined,\n predicates: Predicate[],\n): boolean {\n if (!doc) return false;\n return documentMatchesPredicates(doc, predicates);\n}\n\n/**\n * Resolve the outbox op for one profile after a host row mutation.\n * See plan 046 grill lock (leave → delete; hard-delete outside clamp → suppress).\n */\nexport function resolveSyncMembershipOp(args: {\n predicates: Predicate[];\n before: SyncRowSnapshot;\n after: SyncRowSnapshot;\n requestedOp: SyncMembershipOp;\n}): SyncMembershipOp | null {\n const { predicates, before, after, requestedOp } = args;\n if (predicates.length === 0) return requestedOp;\n\n const beforeMatch = documentMatchesSyncClamp(before, predicates);\n const afterMatch = documentMatchesSyncClamp(after, predicates);\n\n if (requestedOp === \"delete\") {\n return beforeMatch ? \"delete\" : null;\n }\n\n if (afterMatch) return \"upsert\";\n if (beforeMatch) return \"delete\";\n return null;\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */
2
+ export declare const RESERVED_TENANT_SLUGS: readonly ["manage", "deploy", "www"];
3
+ /** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */
4
+ export declare const TENANT_SLUG_RE: RegExp;
5
+ export declare function isReservedTenantSlug(slug: string): boolean;
6
+ export declare function isValidTenantSlug(slug: string): boolean;
7
+ /** Strip optional port from a Host header or `window.location.host` value. */
8
+ export declare function hostnameFromHostHeader(host: string): string;
9
+ /**
10
+ * Extract tenant slug from a hostname.
11
+ * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.
12
+ * Returns null for apex, reserved, invalid, or unrecognized hosts.
13
+ */
14
+ export declare function tenantSlugFromHost(host: string): string | null;
15
+ export type TenantPublicHostEnv = "prod" | "local";
16
+ /** Public hostname for a tenant slug (no scheme). */
17
+ export declare function tenantPublicHost(slug: string, env?: TenantPublicHostEnv): string;
18
+ /** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */
19
+ export declare const PREVIEW_TENANT_SLUG_RE: RegExp;
20
+ export declare function isValidPreviewTenantSlug(slug: string): boolean;
21
+ /**
22
+ * Extract preview tenant slug from hostname (`preview-12.columnus.app`).
23
+ * Returns null for non-preview or invalid hosts.
24
+ */
25
+ export declare function previewSlugFromHost(host: string): string | null;
26
+ /**
27
+ * Resolve either a standard tenant slug or a registered preview slug from a host.
28
+ * Does not check Management DB — only host shape.
29
+ */
30
+ export declare function anyTenantSlugFromHost(host: string): string | null;
@@ -0,0 +1,60 @@
1
+ // packages/@columnus/shared/src/tenantHosts.ts
2
+ var RESERVED_TENANT_SLUGS = ["manage", "deploy", "www"];
3
+ var RESERVED_SLUG_SET = new Set(RESERVED_TENANT_SLUGS);
4
+ var TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ function isReservedTenantSlug(slug) {
6
+ const normalized = slug.trim().toLowerCase();
7
+ if (!normalized) return true;
8
+ if (RESERVED_SLUG_SET.has(normalized)) return true;
9
+ if (normalized.startsWith("preview-")) return true;
10
+ return false;
11
+ }
12
+ function isValidTenantSlug(slug) {
13
+ const trimmed = slug.trim();
14
+ if (!TENANT_SLUG_RE.test(trimmed)) return false;
15
+ return !isReservedTenantSlug(trimmed);
16
+ }
17
+ function hostnameFromHostHeader(host) {
18
+ const trimmed = host.trim().toLowerCase();
19
+ if (!trimmed) return "";
20
+ if (trimmed.startsWith("[")) {
21
+ const end = trimmed.indexOf("]");
22
+ return end === -1 ? trimmed : trimmed.slice(1, end);
23
+ }
24
+ const colon = trimmed.lastIndexOf(":");
25
+ if (colon > -1 && /^\d+$/.test(trimmed.slice(colon + 1))) {
26
+ return trimmed.slice(0, colon);
27
+ }
28
+ return trimmed;
29
+ }
30
+ function tenantSlugFromHost(host) {
31
+ const hostname = hostnameFromHostHeader(host);
32
+ const match = hostname.match(/^([a-z0-9-]+)\.columnus\.(app|localhost)$/);
33
+ if (!match) return null;
34
+ const slug = match[1];
35
+ if (!slug || !isValidTenantSlug(slug)) return null;
36
+ return slug;
37
+ }
38
+ function tenantPublicHost(slug, env = "prod") {
39
+ const normalized = slug.trim().toLowerCase();
40
+ return env === "local" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;
41
+ }
42
+ var PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;
43
+ function isValidPreviewTenantSlug(slug) {
44
+ return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());
45
+ }
46
+ function previewSlugFromHost(host) {
47
+ const hostname = hostnameFromHostHeader(host);
48
+ const match = hostname.match(/^(preview-[a-z0-9-]+)\.columnus\.(app|localhost)$/);
49
+ if (!match) return null;
50
+ const slug = match[1];
51
+ if (!slug || !isValidPreviewTenantSlug(slug)) return null;
52
+ return slug;
53
+ }
54
+ function anyTenantSlugFromHost(host) {
55
+ return tenantSlugFromHost(host) ?? previewSlugFromHost(host);
56
+ }
57
+
58
+ export { PREVIEW_TENANT_SLUG_RE, RESERVED_TENANT_SLUGS, TENANT_SLUG_RE, anyTenantSlugFromHost, hostnameFromHostHeader, isReservedTenantSlug, isValidPreviewTenantSlug, isValidTenantSlug, previewSlugFromHost, tenantPublicHost, tenantSlugFromHost };
59
+ //# sourceMappingURL=tenantHosts.js.map
60
+ //# sourceMappingURL=tenantHosts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tenantHosts.ts"],"names":[],"mappings":"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","file":"tenantHosts.js","sourcesContent":["/** Reserved first labels under `*.columnus.app` / `*.columnus.localhost` — not tenants. */\nexport const RESERVED_TENANT_SLUGS = [\"manage\", \"deploy\", \"www\"] as const;\n\nconst RESERVED_SLUG_SET = new Set<string>(RESERVED_TENANT_SLUGS);\n\n/** Lowercase slug: letters/digits with optional hyphen segments (e.g. `app`, `acme-prod`). */\nexport const TENANT_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isReservedTenantSlug(slug: string): boolean {\n const normalized = slug.trim().toLowerCase();\n if (!normalized) return true;\n if (RESERVED_SLUG_SET.has(normalized)) return true;\n if (normalized.startsWith(\"preview-\")) return true;\n return false;\n}\n\nexport function isValidTenantSlug(slug: string): boolean {\n const trimmed = slug.trim();\n // Require already-lowercase slugs (hosts are normalized before parse).\n if (!TENANT_SLUG_RE.test(trimmed)) return false;\n return !isReservedTenantSlug(trimmed);\n}\n\n/** Strip optional port from a Host header or `window.location.host` value. */\nexport function hostnameFromHostHeader(host: string): string {\n const trimmed = host.trim().toLowerCase();\n if (!trimmed) return \"\";\n // IPv6 bracket form is not used for tenant hosts; strip :port for hostname:port.\n if (trimmed.startsWith(\"[\")) {\n const end = trimmed.indexOf(\"]\");\n return end === -1 ? trimmed : trimmed.slice(1, end);\n }\n const colon = trimmed.lastIndexOf(\":\");\n if (colon > -1 && /^\\d+$/.test(trimmed.slice(colon + 1))) {\n return trimmed.slice(0, colon);\n }\n return trimmed;\n}\n\n/**\n * Extract tenant slug from a hostname.\n * Supports `{slug}.columnus.app` and `{slug}.columnus.localhost`.\n * Returns null for apex, reserved, invalid, or unrecognized hosts.\n */\nexport function tenantSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^([a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidTenantSlug(slug)) return null;\n return slug;\n}\n\nexport type TenantPublicHostEnv = \"prod\" | \"local\";\n\n/** Public hostname for a tenant slug (no scheme). */\nexport function tenantPublicHost(slug: string, env: TenantPublicHostEnv = \"prod\"): string {\n const normalized = slug.trim().toLowerCase();\n return env === \"local\" ? `${normalized}.columnus.localhost` : `${normalized}.columnus.app`;\n}\n\n/** Preview tenant slug: `preview-<label>` (e.g. `preview-12`). */\nexport const PREVIEW_TENANT_SLUG_RE = /^preview-[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nexport function isValidPreviewTenantSlug(slug: string): boolean {\n return PREVIEW_TENANT_SLUG_RE.test(slug.trim().toLowerCase());\n}\n\n/**\n * Extract preview tenant slug from hostname (`preview-12.columnus.app`).\n * Returns null for non-preview or invalid hosts.\n */\nexport function previewSlugFromHost(host: string): string | null {\n const hostname = hostnameFromHostHeader(host);\n const match = hostname.match(/^(preview-[a-z0-9-]+)\\.columnus\\.(app|localhost)$/);\n if (!match) return null;\n const slug = match[1];\n if (!slug || !isValidPreviewTenantSlug(slug)) return null;\n return slug;\n}\n\n/**\n * Resolve either a standard tenant slug or a registered preview slug from a host.\n * Does not check Management DB — only host shape.\n */\nexport function anyTenantSlugFromHost(host: string): string | null {\n return tenantSlugFromHost(host) ?? previewSlugFromHost(host);\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * App-level encryption for management-plane tenant secrets (deploy keys).
3
+ * AES-256-GCM, random 12-byte IV, version byte prefix in the ciphertext blob.
4
+ *
5
+ * KEK: set `TENANT_SECRETS_KEK` on the management Convex deployment to 32 raw
6
+ * bytes encoded as standard/base64url base64 or 64-char hex
7
+ * (`openssl rand -base64 32`).
8
+ */
9
+ export declare const TENANT_SECRETS_KEK_ENV = "TENANT_SECRETS_KEK";
10
+ /** Ciphertext format / algorithm version (stored as first byte + `keyVersion`). */
11
+ export declare const TENANT_SECRET_CRYPTO_VERSION = 1;
12
+ export type EncryptedTenantSecret = {
13
+ /** base64url(version || iv || ciphertext+tag) */
14
+ ciphertext: string;
15
+ keyVersion: number;
16
+ };
17
+ type ProcessEnvLike = Record<string, string | undefined>;
18
+ /** Parse a 32-byte KEK from base64, base64url, or hex. */
19
+ export declare function parseTenantSecretsKek(raw: string): Uint8Array;
20
+ export declare function readTenantSecretsKekFromEnv(env: ProcessEnvLike): Uint8Array;
21
+ /**
22
+ * Encrypt a tenant secret (e.g. Convex deploy key).
23
+ * Returns opaque ciphertext for `tenant_secrets.convexDeployKeyCiphertext`
24
+ * and `keyVersion` for the schema column.
25
+ */
26
+ export declare function encryptTenantSecret(plaintext: string, kek: Uint8Array): Promise<EncryptedTenantSecret>;
27
+ /** Decrypt a blob produced by {@link encryptTenantSecret}. */
28
+ export declare function decryptTenantSecret(ciphertext: string, kek: Uint8Array): Promise<string>;
29
+ /** Generate a new KEK suitable for `TENANT_SECRETS_KEK` (base64). */
30
+ export declare function generateTenantSecretsKekBase64(): string;
31
+ export {};