@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,11 @@
1
+ /** URL-safe profile slug for `api.columnus.read.{slug}.*` (plan 004). */
2
+ export declare const API_PROFILE_SLUG_RE: RegExp;
3
+ export declare function isApiProfileSlug(slug: string): boolean;
4
+ /** Derive a slug from a human display name. */
5
+ export declare function slugifyApiProfileName(name: string): string;
6
+ export declare function assertApiProfileSlug(slug: string): void;
7
+ /** Reject profile slug edits after create (plan 006 D13). */
8
+ export declare function profileSlugChangeError(existingSlug: string, nextSlug: string | undefined): string | undefined;
9
+ export declare function assertProfileSlugUnchanged(existingSlug: string, nextSlug: string | undefined): void;
10
+ /** Convex module path segment (`columnus/read/{segment}/…`). Hyphens are invalid in paths. */
11
+ export declare function apiProfileSlugToConvexModuleSegment(slug: string): string;
@@ -0,0 +1,39 @@
1
+ // packages/@columnus/shared/src/apiProfileSlug.ts
2
+ var API_PROFILE_SLUG_RE = /^[a-z][a-z0-9-]{0,47}$/;
3
+ function isApiProfileSlug(slug) {
4
+ return API_PROFILE_SLUG_RE.test(slug);
5
+ }
6
+ function slugifyApiProfileName(name) {
7
+ const lowered = name.trim().toLowerCase();
8
+ const hyphenated = lowered.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
9
+ let slug = hyphenated.slice(0, 48);
10
+ if (!slug) slug = "profile";
11
+ if (!/^[a-z]/.test(slug)) slug = `p-${slug}`.slice(0, 48);
12
+ return slug.slice(0, 48);
13
+ }
14
+ function assertApiProfileSlug(slug) {
15
+ if (!isApiProfileSlug(slug)) {
16
+ throw new Error(
17
+ "Profile slug must start with a letter and contain only lowercase letters, numbers, and hyphens (max 48 characters)."
18
+ );
19
+ }
20
+ }
21
+ function profileSlugChangeError(existingSlug, nextSlug) {
22
+ if (nextSlug === void 0) return void 0;
23
+ const trimmed = nextSlug.trim();
24
+ if (trimmed && trimmed !== existingSlug) {
25
+ return "Profile slug cannot be changed after creation";
26
+ }
27
+ return void 0;
28
+ }
29
+ function assertProfileSlugUnchanged(existingSlug, nextSlug) {
30
+ const message = profileSlugChangeError(existingSlug, nextSlug);
31
+ if (message) throw new Error(message);
32
+ }
33
+ function apiProfileSlugToConvexModuleSegment(slug) {
34
+ return slug.replace(/-/g, "_");
35
+ }
36
+
37
+ export { API_PROFILE_SLUG_RE, apiProfileSlugToConvexModuleSegment, assertApiProfileSlug, assertProfileSlugUnchanged, isApiProfileSlug, profileSlugChangeError, slugifyApiProfileName };
38
+ //# sourceMappingURL=apiProfileSlug.js.map
39
+ //# sourceMappingURL=apiProfileSlug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apiProfileSlug.ts"],"names":[],"mappings":";AACO,IAAM,mBAAA,GAAsB;AAE5B,SAAS,iBAAiB,IAAA,EAAuB;AACtD,EAAA,OAAO,mBAAA,CAAoB,KAAK,IAAI,CAAA;AACtC;AAGO,SAAS,sBAAsB,IAAA,EAAsB;AAC1D,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AACxC,EAAA,MAAM,UAAA,GAAa,OAAA,CAChB,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAC1B,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CACtB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA;AACrB,EAAA,IAAI,IAAA,GAAO,UAAA,CAAW,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACjC,EAAA,IAAI,CAAC,MAAM,IAAA,GAAO,SAAA;AAClB,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,GAAO,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA,CAAG,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACxD,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACzB;AAEO,SAAS,qBAAqB,IAAA,EAAoB;AACvD,EAAA,IAAI,CAAC,gBAAA,CAAiB,IAAI,CAAA,EAAG;AAC3B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACF;AAGO,SAAS,sBAAA,CACd,cACA,QAAA,EACoB;AACpB,EAAA,IAAI,QAAA,KAAa,QAAW,OAAO,MAAA;AACnC,EAAA,MAAM,OAAA,GAAU,SAAS,IAAA,EAAK;AAC9B,EAAA,IAAI,OAAA,IAAW,YAAY,YAAA,EAAc;AACvC,IAAA,OAAO,+CAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,0BAAA,CACd,cACA,QAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,sBAAA,CAAuB,YAAA,EAAc,QAAQ,CAAA;AAC7D,EAAA,IAAI,OAAA,EAAS,MAAM,IAAI,KAAA,CAAM,OAAO,CAAA;AACtC;AAGO,SAAS,oCAAoC,IAAA,EAAsB;AACxE,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AAC/B","file":"apiProfileSlug.js","sourcesContent":["/** URL-safe profile slug for `api.columnus.read.{slug}.*` (plan 004). */\nexport const API_PROFILE_SLUG_RE = /^[a-z][a-z0-9-]{0,47}$/;\n\nexport function isApiProfileSlug(slug: string): boolean {\n return API_PROFILE_SLUG_RE.test(slug);\n}\n\n/** Derive a slug from a human display name. */\nexport function slugifyApiProfileName(name: string): string {\n const lowered = name.trim().toLowerCase();\n const hyphenated = lowered\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .replace(/-+/g, \"-\");\n let slug = hyphenated.slice(0, 48);\n if (!slug) slug = \"profile\";\n if (!/^[a-z]/.test(slug)) slug = `p-${slug}`.slice(0, 48);\n return slug.slice(0, 48);\n}\n\nexport function assertApiProfileSlug(slug: string): void {\n if (!isApiProfileSlug(slug)) {\n throw new Error(\n \"Profile slug must start with a letter and contain only lowercase letters, numbers, and hyphens (max 48 characters).\",\n );\n }\n}\n\n/** Reject profile slug edits after create (plan 006 D13). */\nexport function profileSlugChangeError(\n existingSlug: string,\n nextSlug: string | undefined,\n): string | undefined {\n if (nextSlug === undefined) return undefined;\n const trimmed = nextSlug.trim();\n if (trimmed && trimmed !== existingSlug) {\n return \"Profile slug cannot be changed after creation\";\n }\n return undefined;\n}\n\nexport function assertProfileSlugUnchanged(\n existingSlug: string,\n nextSlug: string | undefined,\n): void {\n const message = profileSlugChangeError(existingSlug, nextSlug);\n if (message) throw new Error(message);\n}\n\n/** Convex module path segment (`columnus/read/{segment}/…`). Hyphens are invalid in paths. */\nexport function apiProfileSlugToConvexModuleSegment(slug: string): string {\n return slug.replace(/-/g, \"_\");\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /** Read API token prefix (see plans/done/004-api-read-access.md). */
2
+ export declare const API_TOKEN_PREFIX = "col_";
3
+ export declare function isApiTokenFormat(token: string): boolean;
4
+ export declare function apiTokenPrefix(token: string): string;
5
+ export declare function generateApiToken(): string;
6
+ export declare function hashApiToken(token: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ // packages/@columnus/shared/src/apiToken.ts
2
+ var API_TOKEN_PREFIX = "col_";
3
+ var TOKEN_BODY_RE = /^col_[A-Za-z0-9_-]{20,}$/;
4
+ function isApiTokenFormat(token) {
5
+ return TOKEN_BODY_RE.test(token.trim());
6
+ }
7
+ function apiTokenPrefix(token) {
8
+ const trimmed = token.trim();
9
+ if (trimmed.length <= API_TOKEN_PREFIX.length + 4)
10
+ return trimmed.slice(0, API_TOKEN_PREFIX.length + 4);
11
+ return `${trimmed.slice(0, API_TOKEN_PREFIX.length + 7)}\u2026`;
12
+ }
13
+ function generateApiToken() {
14
+ const bytes = new Uint8Array(24);
15
+ crypto.getRandomValues(bytes);
16
+ const body = base64UrlEncode(bytes);
17
+ return `${API_TOKEN_PREFIX}${body}`;
18
+ }
19
+ async function hashApiToken(token) {
20
+ const data = new TextEncoder().encode(token.trim());
21
+ const digest = await crypto.subtle.digest("SHA-256", data);
22
+ return hexEncode(new Uint8Array(digest));
23
+ }
24
+ function base64UrlEncode(bytes) {
25
+ let binary = "";
26
+ for (const byte of bytes) binary += String.fromCharCode(byte);
27
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
28
+ }
29
+ function hexEncode(bytes) {
30
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
31
+ }
32
+
33
+ export { API_TOKEN_PREFIX, apiTokenPrefix, generateApiToken, hashApiToken, isApiTokenFormat };
34
+ //# sourceMappingURL=apiToken.js.map
35
+ //# sourceMappingURL=apiToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apiToken.ts"],"names":[],"mappings":";AACO,IAAM,gBAAA,GAAmB;AAEhC,IAAM,aAAA,GAAgB,0BAAA;AAEf,SAAS,iBAAiB,KAAA,EAAwB;AACvD,EAAA,OAAO,aAAA,CAAc,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAA;AACxC;AAEO,SAAS,eAAe,KAAA,EAAuB;AACpD,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,gBAAA,CAAiB,MAAA,GAAS,CAAA;AAC9C,IAAA,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,gBAAA,CAAiB,SAAS,CAAC,CAAA;AACrD,EAAA,OAAO,GAAG,OAAA,CAAQ,KAAA,CAAM,GAAG,gBAAA,CAAiB,MAAA,GAAS,CAAC,CAAC,CAAA,MAAA,CAAA;AACzD;AAEO,SAAS,gBAAA,GAA2B;AACzC,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,MAAM,IAAA,GAAO,gBAAgB,KAAK,CAAA;AAClC,EAAA,OAAO,CAAA,EAAG,gBAAgB,CAAA,EAAG,IAAI,CAAA,CAAA;AACnC;AAEA,eAAsB,aAAa,KAAA,EAAgC;AACjE,EAAA,MAAM,OAAO,IAAI,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA;AAClD,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AACzD,EAAA,OAAO,SAAA,CAAU,IAAI,UAAA,CAAW,MAAM,CAAC,CAAA;AACzC;AAEA,SAAS,gBAAgB,KAAA,EAA2B;AAClD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAChF;AAEA,SAAS,UAAU,KAAA,EAA2B;AAC5C,EAAA,OAAO,CAAC,GAAG,KAAK,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AACvE","file":"apiToken.js","sourcesContent":["/** Read API token prefix (see plans/done/004-api-read-access.md). */\nexport const API_TOKEN_PREFIX = \"col_\";\n\nconst TOKEN_BODY_RE = /^col_[A-Za-z0-9_-]{20,}$/;\n\nexport function isApiTokenFormat(token: string): boolean {\n return TOKEN_BODY_RE.test(token.trim());\n}\n\nexport function apiTokenPrefix(token: string): string {\n const trimmed = token.trim();\n if (trimmed.length <= API_TOKEN_PREFIX.length + 4)\n return trimmed.slice(0, API_TOKEN_PREFIX.length + 4);\n return `${trimmed.slice(0, API_TOKEN_PREFIX.length + 7)}…`;\n}\n\nexport function generateApiToken(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n const body = base64UrlEncode(bytes);\n return `${API_TOKEN_PREFIX}${body}`;\n}\n\nexport async function hashApiToken(token: string): Promise<string> {\n const data = new TextEncoder().encode(token.trim());\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\n return hexEncode(new Uint8Array(digest));\n}\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/g, \"\");\n}\n\nfunction hexEncode(bytes: Uint8Array): string {\n return [...bytes].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"]}
@@ -0,0 +1,8 @@
1
+ export declare function isConvexJwt(token: string): boolean;
2
+ export declare function isJwtExpired(token: string, skewSec?: number): boolean;
3
+ export type SignInViaEmailOtpOptions = {
4
+ siteUrl: string;
5
+ email: string;
6
+ otp?: string;
7
+ };
8
+ export declare function signInViaEmailOtp({ siteUrl, email, otp, }: SignInViaEmailOtpOptions): Promise<string>;
@@ -0,0 +1,57 @@
1
+ // packages/@columnus/shared/src/betterAuthSession.ts
2
+ function isConvexJwt(token) {
3
+ return token.split(".").length === 3;
4
+ }
5
+ function isJwtExpired(token, skewSec = 60) {
6
+ const parts = token.split(".");
7
+ if (parts.length !== 3) return true;
8
+ try {
9
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
10
+ if (typeof payload.exp !== "number") return true;
11
+ return payload.exp <= Math.floor(Date.now() / 1e3) + skewSec;
12
+ } catch {
13
+ return true;
14
+ }
15
+ }
16
+ async function signInViaEmailOtp({
17
+ siteUrl,
18
+ email,
19
+ otp = process.env.DEV_OTP_CODE?.trim() || "000000"
20
+ }) {
21
+ const base = siteUrl.replace(/\/$/, "");
22
+ const sendRes = await fetch(`${base}/api/auth/email-otp/send-verification-otp`, {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({ email, type: "sign-in" })
26
+ });
27
+ if (!sendRes.ok) {
28
+ throw new Error(`send-verification-otp failed: ${sendRes.status} ${await sendRes.text()}`);
29
+ }
30
+ const signInRes = await fetch(`${base}/api/auth/sign-in/email-otp`, {
31
+ method: "POST",
32
+ headers: { "content-type": "application/json" },
33
+ body: JSON.stringify({ email, otp })
34
+ });
35
+ if (!signInRes.ok) {
36
+ throw new Error(`sign-in/email-otp failed: ${signInRes.status} ${await signInRes.text()}`);
37
+ }
38
+ const body = await signInRes.json();
39
+ if (!body.token) {
40
+ throw new Error("sign-in/email-otp response missing token");
41
+ }
42
+ const convexTokenRes = await fetch(`${base}/api/auth/convex/token`, {
43
+ headers: { Authorization: `Bearer ${body.token}` }
44
+ });
45
+ if (!convexTokenRes.ok) {
46
+ throw new Error(`convex/token failed: ${convexTokenRes.status} ${await convexTokenRes.text()}`);
47
+ }
48
+ const convexBody = await convexTokenRes.json();
49
+ if (!convexBody.token) {
50
+ throw new Error("convex/token response missing token");
51
+ }
52
+ return convexBody.token;
53
+ }
54
+
55
+ export { isConvexJwt, isJwtExpired, signInViaEmailOtp };
56
+ //# sourceMappingURL=betterAuthSession.js.map
57
+ //# sourceMappingURL=betterAuthSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/betterAuthSession.ts"],"names":[],"mappings":"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","file":"betterAuthSession.js","sourcesContent":["export function isConvexJwt(token: string): boolean {\n return token.split(\".\").length === 3;\n}\n\nexport function isJwtExpired(token: string, skewSec = 60): boolean {\n const parts = token.split(\".\");\n if (parts.length !== 3) return true;\n try {\n const payload = JSON.parse(Buffer.from(parts[1] ?? \"\", \"base64url\").toString(\"utf8\")) as {\n exp?: number;\n };\n if (typeof payload.exp !== \"number\") return true;\n return payload.exp <= Math.floor(Date.now() / 1000) + skewSec;\n } catch {\n return true;\n }\n}\n\nexport type SignInViaEmailOtpOptions = {\n siteUrl: string;\n email: string;\n otp?: string;\n};\n\nexport async function signInViaEmailOtp({\n siteUrl,\n email,\n otp = process.env.DEV_OTP_CODE?.trim() || \"000000\",\n}: SignInViaEmailOtpOptions): Promise<string> {\n const base = siteUrl.replace(/\\/$/, \"\");\n\n const sendRes = await fetch(`${base}/api/auth/email-otp/send-verification-otp`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ email, type: \"sign-in\" }),\n });\n if (!sendRes.ok) {\n throw new Error(`send-verification-otp failed: ${sendRes.status} ${await sendRes.text()}`);\n }\n\n const signInRes = await fetch(`${base}/api/auth/sign-in/email-otp`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ email, otp }),\n });\n if (!signInRes.ok) {\n throw new Error(`sign-in/email-otp failed: ${signInRes.status} ${await signInRes.text()}`);\n }\n\n const body = (await signInRes.json()) as { token?: string };\n if (!body.token) {\n throw new Error(\"sign-in/email-otp response missing token\");\n }\n\n const convexTokenRes = await fetch(`${base}/api/auth/convex/token`, {\n headers: { Authorization: `Bearer ${body.token}` },\n });\n if (!convexTokenRes.ok) {\n throw new Error(`convex/token failed: ${convexTokenRes.status} ${await convexTokenRes.text()}`);\n }\n const convexBody = (await convexTokenRes.json()) as { token?: string };\n if (!convexBody.token) {\n throw new Error(\"convex/token response missing token\");\n }\n return convexBody.token;\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Trusted request host headers for Columnus BFF tenant resolution (D35/D41).
3
+ * Prefer Forwarded / X-Forwarded-Host when present, else Host.
4
+ * Always trust these headers in the web/BFF process — operators must not expose
5
+ * the container publicly without Traefik/Coolify/tunnel in front.
6
+ */
7
+ export type RequestHostHeaders = {
8
+ /** RFC 7239 `Forwarded` header value. */
9
+ forwarded?: string | null;
10
+ /** `X-Forwarded-Host` header value. */
11
+ xForwardedHost?: string | null;
12
+ /** `Host` header value. */
13
+ host?: string | null;
14
+ };
15
+ export type TrustedHostResult = {
16
+ ok: true;
17
+ hostname: string;
18
+ } | {
19
+ ok: false;
20
+ error: string;
21
+ };
22
+ /**
23
+ * Extract a single host value from a header that may be multi-valued.
24
+ * Returns null when empty; rejects comma-separated lists as ambiguous.
25
+ */
26
+ export declare function singleHeaderHostValue(raw: string | null | undefined): TrustedHostResult | null;
27
+ /**
28
+ * Parse the `host=` parameter from an RFC 7239 Forwarded header entry.
29
+ * Quoted values are unescaped; ports are stripped via hostnameFromHostHeader.
30
+ */
31
+ export declare function hostFromForwardedHeader(forwarded: string): TrustedHostResult | null;
32
+ /**
33
+ * Resolve the trusted request hostname for tenant identity (D3/D35/D41).
34
+ * Never accepts a client-supplied tenant slug — only host headers.
35
+ */
36
+ export declare function extractTrustedHostname(headers: RequestHostHeaders): TrustedHostResult;
@@ -0,0 +1,74 @@
1
+ // packages/@columnus/shared/src/tenantHosts.ts
2
+ var RESERVED_TENANT_SLUGS = ["manage", "deploy", "www"];
3
+ new Set(RESERVED_TENANT_SLUGS);
4
+ function hostnameFromHostHeader(host) {
5
+ const trimmed = host.trim().toLowerCase();
6
+ if (!trimmed) return "";
7
+ if (trimmed.startsWith("[")) {
8
+ const end = trimmed.indexOf("]");
9
+ return end === -1 ? trimmed : trimmed.slice(1, end);
10
+ }
11
+ const colon = trimmed.lastIndexOf(":");
12
+ if (colon > -1 && /^\d+$/.test(trimmed.slice(colon + 1))) {
13
+ return trimmed.slice(0, colon);
14
+ }
15
+ return trimmed;
16
+ }
17
+
18
+ // packages/@columnus/shared/src/bffHost.ts
19
+ function singleHeaderHostValue(raw) {
20
+ if (raw == null) return null;
21
+ const trimmed = raw.trim();
22
+ if (!trimmed) return null;
23
+ if (trimmed.includes(",")) {
24
+ return { ok: false, error: "ambiguous multi-value host header" };
25
+ }
26
+ const hostname = hostnameFromHostHeader(trimmed);
27
+ if (!hostname) return { ok: false, error: "empty hostname" };
28
+ return { ok: true, hostname };
29
+ }
30
+ function hostFromForwardedHeader(forwarded) {
31
+ const trimmed = forwarded.trim();
32
+ if (!trimmed) return null;
33
+ const entries = trimmed.split(",");
34
+ const hosts = [];
35
+ for (const entry of entries) {
36
+ const host = hostParamFromForwardedEntry(entry);
37
+ if (host) hosts.push(host);
38
+ }
39
+ if (hosts.length === 0) return null;
40
+ const unique = new Set(hosts.map((h) => hostnameFromHostHeader(h)).filter(Boolean));
41
+ if (unique.size > 1) {
42
+ return { ok: false, error: "ambiguous multi-value Forwarded host" };
43
+ }
44
+ const [hostname] = unique;
45
+ if (!hostname) return { ok: false, error: "empty hostname" };
46
+ return { ok: true, hostname };
47
+ }
48
+ function hostParamFromForwardedEntry(entry) {
49
+ for (const part of entry.split(";")) {
50
+ const eq = part.indexOf("=");
51
+ if (eq === -1) continue;
52
+ const key = part.slice(0, eq).trim().toLowerCase();
53
+ if (key !== "host") continue;
54
+ let value = part.slice(eq + 1).trim();
55
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
56
+ value = value.slice(1, -1).replace(/\\(.)/g, "$1");
57
+ }
58
+ return value || null;
59
+ }
60
+ return null;
61
+ }
62
+ function extractTrustedHostname(headers) {
63
+ const fromForwarded = headers.forwarded != null && headers.forwarded.trim() !== "" ? hostFromForwardedHeader(headers.forwarded) : null;
64
+ if (fromForwarded) return fromForwarded;
65
+ const fromXfh = singleHeaderHostValue(headers.xForwardedHost);
66
+ if (fromXfh) return fromXfh;
67
+ const fromHost = singleHeaderHostValue(headers.host);
68
+ if (fromHost) return fromHost;
69
+ return { ok: false, error: "missing Host header" };
70
+ }
71
+
72
+ export { extractTrustedHostname, hostFromForwardedHeader, singleHeaderHostValue };
73
+ //# sourceMappingURL=bffHost.js.map
74
+ //# sourceMappingURL=bffHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tenantHosts.ts","../src/bffHost.ts"],"names":[],"mappings":"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","file":"bffHost.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","import { hostnameFromHostHeader } from \"./tenantHosts.ts\";\n\n/**\n * Trusted request host headers for Columnus BFF tenant resolution (D35/D41).\n * Prefer Forwarded / X-Forwarded-Host when present, else Host.\n * Always trust these headers in the web/BFF process — operators must not expose\n * the container publicly without Traefik/Coolify/tunnel in front.\n */\nexport type RequestHostHeaders = {\n /** RFC 7239 `Forwarded` header value. */\n forwarded?: string | null;\n /** `X-Forwarded-Host` header value. */\n xForwardedHost?: string | null;\n /** `Host` header value. */\n host?: string | null;\n};\n\nexport type TrustedHostResult = { ok: true; hostname: string } | { ok: false; error: string };\n\n/**\n * Extract a single host value from a header that may be multi-valued.\n * Returns null when empty; rejects comma-separated lists as ambiguous.\n */\nexport function singleHeaderHostValue(raw: string | null | undefined): TrustedHostResult | null {\n if (raw == null) return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n if (trimmed.includes(\",\")) {\n return { ok: false, error: \"ambiguous multi-value host header\" };\n }\n const hostname = hostnameFromHostHeader(trimmed);\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\n/**\n * Parse the `host=` parameter from an RFC 7239 Forwarded header entry.\n * Quoted values are unescaped; ports are stripped via hostnameFromHostHeader.\n */\nexport function hostFromForwardedHeader(forwarded: string): TrustedHostResult | null {\n const trimmed = forwarded.trim();\n if (!trimmed) return null;\n\n const entries = trimmed.split(\",\");\n const hosts: string[] = [];\n for (const entry of entries) {\n const host = hostParamFromForwardedEntry(entry);\n if (host) hosts.push(host);\n }\n if (hosts.length === 0) return null;\n const unique = new Set(hosts.map((h) => hostnameFromHostHeader(h)).filter(Boolean));\n if (unique.size > 1) {\n return { ok: false, error: \"ambiguous multi-value Forwarded host\" };\n }\n const [hostname] = unique;\n if (!hostname) return { ok: false, error: \"empty hostname\" };\n return { ok: true, hostname };\n}\n\nfunction hostParamFromForwardedEntry(entry: string): string | null {\n // Parameters are `;`-separated; values may be quoted.\n for (const part of entry.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim().toLowerCase();\n if (key !== \"host\") continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"') && value.length >= 2) {\n value = value.slice(1, -1).replace(/\\\\(.)/g, \"$1\");\n }\n return value || null;\n }\n return null;\n}\n\n/**\n * Resolve the trusted request hostname for tenant identity (D3/D35/D41).\n * Never accepts a client-supplied tenant slug — only host headers.\n */\nexport function extractTrustedHostname(headers: RequestHostHeaders): TrustedHostResult {\n const fromForwarded =\n headers.forwarded != null && headers.forwarded.trim() !== \"\"\n ? hostFromForwardedHeader(headers.forwarded)\n : null;\n if (fromForwarded) return fromForwarded;\n\n const fromXfh = singleHeaderHostValue(headers.xForwardedHost);\n if (fromXfh) return fromXfh;\n\n const fromHost = singleHeaderHostValue(headers.host);\n if (fromHost) return fromHost;\n\n return { ok: false, error: \"missing Host header\" };\n}\n"]}