@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.
- package/LICENSE +21 -0
- package/dist/apiProfile.d.ts +1072 -0
- package/dist/apiProfile.js +4210 -0
- package/dist/apiProfile.js.map +1 -0
- package/dist/apiProfileSlug.d.ts +11 -0
- package/dist/apiProfileSlug.js +39 -0
- package/dist/apiProfileSlug.js.map +1 -0
- package/dist/apiToken.d.ts +6 -0
- package/dist/apiToken.js +35 -0
- package/dist/apiToken.js.map +1 -0
- package/dist/betterAuthSession.d.ts +8 -0
- package/dist/betterAuthSession.js +57 -0
- package/dist/betterAuthSession.js.map +1 -0
- package/dist/bffHost.d.ts +36 -0
- package/dist/bffHost.js +74 -0
- package/dist/bffHost.js.map +1 -0
- package/dist/bffProtocol.d.ts +1357 -0
- package/dist/bffProtocol.js +4362 -0
- package/dist/bffProtocol.js.map +1 -0
- package/dist/bffRedact.d.ts +30 -0
- package/dist/bffRedact.js +55 -0
- package/dist/bffRedact.js.map +1 -0
- package/dist/ciPreviewEnv.d.ts +49 -0
- package/dist/ciPreviewEnv.js +143 -0
- package/dist/ciPreviewEnv.js.map +1 -0
- package/dist/clipboardTransfer.d.ts +25 -0
- package/dist/clipboardTransfer.js +4862 -0
- package/dist/clipboardTransfer.js.map +1 -0
- package/dist/convexSiteUrl.d.ts +73 -0
- package/dist/convexSiteUrl.js +137 -0
- package/dist/convexSiteUrl.js.map +1 -0
- package/dist/csv.d.ts +54 -0
- package/dist/csv.js +5499 -0
- package/dist/csv.js.map +1 -0
- package/dist/dataTransfer.d.ts +21 -0
- package/dist/dataTransfer.js +74 -0
- package/dist/dataTransfer.js.map +1 -0
- package/dist/dateValues.d.ts +18 -0
- package/dist/dateValues.js +73 -0
- package/dist/dateValues.js.map +1 -0
- package/dist/deployOutput.d.ts +3 -0
- package/dist/deployOutput.js +23 -0
- package/dist/deployOutput.js.map +1 -0
- package/dist/displayLabel.d.ts +13 -0
- package/dist/displayLabel.js +4342 -0
- package/dist/displayLabel.js.map +1 -0
- package/dist/displayTemplateGenerator.d.ts +11 -0
- package/dist/displayTemplateGenerator.js +98 -0
- package/dist/displayTemplateGenerator.js.map +1 -0
- package/dist/displayTypes.d.ts +10 -0
- package/dist/displayTypes.js +3 -0
- package/dist/displayTypes.js.map +1 -0
- package/dist/dotenv.d.ts +8 -0
- package/dist/dotenv.js +61 -0
- package/dist/dotenv.js.map +1 -0
- package/dist/erdLayout.d.ts +27 -0
- package/dist/erdLayout.js +78 -0
- package/dist/erdLayout.js.map +1 -0
- package/dist/fieldTypeFormat.d.ts +5 -0
- package/dist/fieldTypeFormat.js +63 -0
- package/dist/fieldTypeFormat.js.map +1 -0
- package/dist/fieldTypeMeta.d.ts +50 -0
- package/dist/fieldTypeMeta.js +84 -0
- package/dist/fieldTypeMeta.js.map +1 -0
- package/dist/fieldValueEqual.d.ts +4 -0
- package/dist/fieldValueEqual.js +58 -0
- package/dist/fieldValueEqual.js.map +1 -0
- package/dist/importMatch.d.ts +80 -0
- package/dist/importMatch.js +5643 -0
- package/dist/importMatch.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +10647 -0
- package/dist/index.js.map +1 -0
- package/dist/inferIdentifier.d.ts +18 -0
- package/dist/inferIdentifier.js +49 -0
- package/dist/inferIdentifier.js.map +1 -0
- package/dist/keyboard.d.ts +16 -0
- package/dist/keyboard.js +71 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/manifestDeploy.d.ts +27 -0
- package/dist/manifestDeploy.js +16 -0
- package/dist/manifestDeploy.js.map +1 -0
- package/dist/manifestHash.d.ts +11 -0
- package/dist/manifestHash.js +35 -0
- package/dist/manifestHash.js.map +1 -0
- package/dist/previewDeployName.d.ts +26 -0
- package/dist/previewDeployName.js +77 -0
- package/dist/previewDeployName.js.map +1 -0
- package/dist/proposePolicy.d.ts +17 -0
- package/dist/proposePolicy.js +33 -0
- package/dist/proposePolicy.js.map +1 -0
- package/dist/queryBetween.d.ts +20 -0
- package/dist/queryBetween.js +28 -0
- package/dist/queryBetween.js.map +1 -0
- package/dist/queryExecution.d.ts +5 -0
- package/dist/queryExecution.js +58 -0
- package/dist/queryExecution.js.map +1 -0
- package/dist/queryFieldOps.d.ts +66 -0
- package/dist/queryFieldOps.js +138 -0
- package/dist/queryFieldOps.js.map +1 -0
- package/dist/queryMerge.d.ts +29 -0
- package/dist/queryMerge.js +4277 -0
- package/dist/queryMerge.js.map +1 -0
- package/dist/queryPlan.d.ts +97 -0
- package/dist/queryPlan.js +4564 -0
- package/dist/queryPlan.js.map +1 -0
- package/dist/queryStudio.d.ts +25 -0
- package/dist/queryStudio.js +4658 -0
- package/dist/queryStudio.js.map +1 -0
- package/dist/queryTypes.d.ts +402 -0
- package/dist/queryTypes.js +4239 -0
- package/dist/queryTypes.js.map +1 -0
- package/dist/queryValidators.d.ts +99 -0
- package/dist/queryValidators.js +76 -0
- package/dist/queryValidators.js.map +1 -0
- package/dist/readApiQuery.d.ts +10 -0
- package/dist/readApiQuery.js +24 -0
- package/dist/readApiQuery.js.map +1 -0
- package/dist/recordTableColumns.d.ts +21 -0
- package/dist/recordTableColumns.js +4469 -0
- package/dist/recordTableColumns.js.map +1 -0
- package/dist/refDisplayRequests.d.ts +17 -0
- package/dist/refDisplayRequests.js +4877 -0
- package/dist/refDisplayRequests.js.map +1 -0
- package/dist/refGraph.d.ts +23 -0
- package/dist/refGraph.js +102 -0
- package/dist/refGraph.js.map +1 -0
- package/dist/refsRecompute.d.ts +8 -0
- package/dist/refsRecompute.js +12 -0
- package/dist/refsRecompute.js.map +1 -0
- package/dist/richDoc.d.ts +67 -0
- package/dist/richDoc.js +4408 -0
- package/dist/richDoc.js.map +1 -0
- package/dist/schemaGenerator.d.ts +31 -0
- package/dist/schemaGenerator.js +198 -0
- package/dist/schemaGenerator.js.map +1 -0
- package/dist/schemaShrink.d.ts +74 -0
- package/dist/schemaShrink.js +132 -0
- package/dist/schemaShrink.js.map +1 -0
- package/dist/schemaShrinkValidators.d.ts +72 -0
- package/dist/schemaShrinkValidators.js +31 -0
- package/dist/schemaShrinkValidators.js.map +1 -0
- package/dist/schemaTypes.d.ts +454 -0
- package/dist/schemaTypes.js +4557 -0
- package/dist/schemaTypes.js.map +1 -0
- package/dist/share.d.ts +8 -0
- package/dist/share.js +4755 -0
- package/dist/share.js.map +1 -0
- package/dist/shareApiBridge.d.ts +18 -0
- package/dist/shareApiBridge.js +4283 -0
- package/dist/shareApiBridge.js.map +1 -0
- package/dist/shareGuestPolicy.d.ts +14 -0
- package/dist/shareGuestPolicy.js +90 -0
- package/dist/shareGuestPolicy.js.map +1 -0
- package/dist/shareProposal.d.ts +113 -0
- package/dist/shareProposal.js +4810 -0
- package/dist/shareProposal.js.map +1 -0
- package/dist/shareProposalDiff.d.ts +13 -0
- package/dist/shareProposalDiff.js +91 -0
- package/dist/shareProposalDiff.js.map +1 -0
- package/dist/shareScopes.d.ts +14 -0
- package/dist/shareScopes.js +223 -0
- package/dist/shareScopes.js.map +1 -0
- package/dist/shareTokenCrypto.d.ts +9 -0
- package/dist/shareTokenCrypto.js +87 -0
- package/dist/shareTokenCrypto.js.map +1 -0
- package/dist/shareTokens.d.ts +6 -0
- package/dist/shareTokens.js +26 -0
- package/dist/shareTokens.js.map +1 -0
- package/dist/shareTypes.d.ts +107 -0
- package/dist/shareTypes.js +3 -0
- package/dist/shareTypes.js.map +1 -0
- package/dist/shareValidators.d.ts +168 -0
- package/dist/shareValidators.js +124 -0
- package/dist/shareValidators.js.map +1 -0
- package/dist/shareWizard.d.ts +14 -0
- package/dist/shareWizard.js +200 -0
- package/dist/shareWizard.js.map +1 -0
- package/dist/spaceBundle.d.ts +67 -0
- package/dist/spaceBundle.js +4664 -0
- package/dist/spaceBundle.js.map +1 -0
- package/dist/storageFieldValues.d.ts +10 -0
- package/dist/storageFieldValues.js +4421 -0
- package/dist/storageFieldValues.js.map +1 -0
- package/dist/studioBoot.d.ts +49 -0
- package/dist/studioBoot.js +167 -0
- package/dist/studioBoot.js.map +1 -0
- package/dist/syncClampMembership.d.ts +22 -0
- package/dist/syncClampMembership.js +74 -0
- package/dist/syncClampMembership.js.map +1 -0
- package/dist/tenantHosts.d.ts +30 -0
- package/dist/tenantHosts.js +60 -0
- package/dist/tenantHosts.js.map +1 -0
- package/dist/tenantSecretsCrypto.d.ts +31 -0
- package/dist/tenantSecretsCrypto.js +145 -0
- package/dist/tenantSecretsCrypto.js.map +1 -0
- package/dist/useDebouncedCommit.d.ts +11 -0
- package/dist/useDebouncedCommit.js +34 -0
- package/dist/useDebouncedCommit.js.map +1 -0
- package/dist/useDebouncedValue.d.ts +1 -0
- package/dist/useDebouncedValue.js +15 -0
- package/dist/useDebouncedValue.js.map +1 -0
- package/dist/useModifierHeld.d.ts +2 -0
- package/dist/useModifierHeld.js +28 -0
- package/dist/useModifierHeld.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProposalFieldDiff, ProposalRecordFieldDiff } from "./shareTypes.ts";
|
|
2
|
+
export declare function computeProposalFieldDiff(baseline: Record<string, unknown> | null | undefined, fields: Record<string, unknown>): ProposalFieldDiff[];
|
|
3
|
+
/** Merge baseline + patch into the record a reviewer would see after accept. */
|
|
4
|
+
export declare function mergeProposedRecord(baseline: Record<string, unknown> | null | undefined, fields: Record<string, unknown>, op: "insert" | "update" | "delete"): Record<string, unknown>;
|
|
5
|
+
/** Field names for full-record review: schema order when available, else baseline ∪ patch. */
|
|
6
|
+
export declare function proposalDiffFieldNames(tableFields: readonly {
|
|
7
|
+
name: string;
|
|
8
|
+
}[] | undefined, baseline: Record<string, unknown> | null | undefined, fields: Record<string, unknown>): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Full-record field diffs in schema (or key) order, including unchanged fields.
|
|
11
|
+
* `changed` mirrors patch membership / equality — same rules as computeProposalFieldDiff.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeProposalRecordDiff(fieldNames: readonly string[], baseline: Record<string, unknown> | null | undefined, fields: Record<string, unknown>, op: "insert" | "update" | "delete"): ProposalRecordFieldDiff[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/fieldValueEqual.ts
|
|
2
|
+
function bytesToBase64(bytes) {
|
|
3
|
+
let s = "";
|
|
4
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
5
|
+
return btoa(s);
|
|
6
|
+
}
|
|
7
|
+
function jsonReplacer(_key, value) {
|
|
8
|
+
if (typeof value === "bigint") return `${value.toString()}n`;
|
|
9
|
+
if (value instanceof Uint8Array) return { __bytes__: bytesToBase64(value) };
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function normalizeForCompare(value) {
|
|
13
|
+
if (value === void 0 || value === null) return void 0;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function stableJsonEqual(a, b) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(a, jsonReplacer) === JSON.stringify(b, jsonReplacer);
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fieldValuesEqual(a, b) {
|
|
24
|
+
const na = normalizeForCompare(a);
|
|
25
|
+
const nb = normalizeForCompare(b);
|
|
26
|
+
if (Object.is(na, nb)) return true;
|
|
27
|
+
if (na === void 0 && nb === void 0) return true;
|
|
28
|
+
const ta = typeof na;
|
|
29
|
+
const tb = typeof nb;
|
|
30
|
+
if (ta !== tb) {
|
|
31
|
+
if (ta === "bigint" && tb === "string") {
|
|
32
|
+
const naBig = na;
|
|
33
|
+
const nbStr = nb;
|
|
34
|
+
return `${naBig.toString()}n` === nbStr || naBig.toString() === nbStr.replace(/n$/, "");
|
|
35
|
+
}
|
|
36
|
+
if (ta === "string" && tb === "bigint") {
|
|
37
|
+
const naStr = na;
|
|
38
|
+
const nbBig = nb;
|
|
39
|
+
return `${nbBig.toString()}n` === naStr || nbBig.toString() === naStr.replace(/n$/, "");
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (ta === "object" && na !== null && nb !== null) {
|
|
44
|
+
return stableJsonEqual(na, nb);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/@columnus/shared/src/shareProposalDiff.ts
|
|
50
|
+
function computeProposalFieldDiff(baseline, fields) {
|
|
51
|
+
return Object.entries(fields).filter(([field, after]) => !fieldValuesEqual(baseline?.[field], after)).map(([field, after]) => ({ field, before: baseline?.[field], after }));
|
|
52
|
+
}
|
|
53
|
+
function mergeProposedRecord(baseline, fields, op) {
|
|
54
|
+
if (op === "delete") return { ...baseline ?? {} };
|
|
55
|
+
if (op === "insert") return { ...fields };
|
|
56
|
+
return { ...baseline ?? {}, ...fields };
|
|
57
|
+
}
|
|
58
|
+
function proposalDiffFieldNames(tableFields, baseline, fields) {
|
|
59
|
+
if (tableFields && tableFields.length > 0) {
|
|
60
|
+
return tableFields.map((field) => field.name);
|
|
61
|
+
}
|
|
62
|
+
const names = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const key of Object.keys(baseline ?? {})) {
|
|
64
|
+
if (!key.startsWith("_")) names.add(key);
|
|
65
|
+
}
|
|
66
|
+
for (const key of Object.keys(fields)) {
|
|
67
|
+
if (!key.startsWith("_")) names.add(key);
|
|
68
|
+
}
|
|
69
|
+
return [...names];
|
|
70
|
+
}
|
|
71
|
+
function computeProposalRecordDiff(fieldNames, baseline, fields, op) {
|
|
72
|
+
return fieldNames.map((field) => {
|
|
73
|
+
const before = baseline?.[field];
|
|
74
|
+
if (op === "delete") {
|
|
75
|
+
return {
|
|
76
|
+
field,
|
|
77
|
+
before,
|
|
78
|
+
after: void 0,
|
|
79
|
+
changed: before !== void 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const inPatch = Object.hasOwn(fields, field);
|
|
83
|
+
const after = inPatch ? fields[field] : op === "insert" ? void 0 : before;
|
|
84
|
+
const changed = op === "insert" ? inPatch : inPatch && !fieldValuesEqual(before, fields[field]);
|
|
85
|
+
return { field, before, after, changed };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { computeProposalFieldDiff, computeProposalRecordDiff, mergeProposedRecord, proposalDiffFieldNames };
|
|
90
|
+
//# sourceMappingURL=shareProposalDiff.js.map
|
|
91
|
+
//# sourceMappingURL=shareProposalDiff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fieldValueEqual.ts","../src/shareProposalDiff.ts"],"names":[],"mappings":"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","file":"shareProposalDiff.js","sourcesContent":["// Compare Convex field values for pending-edit diffing. Uses the same\n// JSON normalization as csv export so bigint/bytes compare consistently.\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let s = \"\";\n for (const b of bytes) s += String.fromCharCode(b);\n return btoa(s);\n}\n\nfunction jsonReplacer(_key: string, value: unknown): unknown {\n if (typeof value === \"bigint\") return `${value.toString()}n`;\n if (value instanceof Uint8Array) return { __bytes__: bytesToBase64(value) };\n return value;\n}\n\nfunction normalizeForCompare(value: unknown): unknown {\n if (value === undefined || value === null) return undefined;\n return value;\n}\n\nfunction stableJsonEqual(a: unknown, b: unknown): boolean {\n try {\n return JSON.stringify(a, jsonReplacer) === JSON.stringify(b, jsonReplacer);\n } catch {\n return false;\n }\n}\n\n/** True when two stored field values are equivalent (missing ≡ undefined). */\nexport function fieldValuesEqual(a: unknown, b: unknown): boolean {\n const na = normalizeForCompare(a);\n const nb = normalizeForCompare(b);\n if (Object.is(na, nb)) return true;\n if (na === undefined && nb === undefined) return true;\n\n const ta = typeof na;\n const tb = typeof nb;\n if (ta !== tb) {\n // bigint stored as string in some editors vs bigint on server\n if (ta === \"bigint\" && tb === \"string\") {\n const naBig = na as bigint;\n const nbStr = nb as string;\n return `${naBig.toString()}n` === nbStr || naBig.toString() === nbStr.replace(/n$/, \"\");\n }\n if (ta === \"string\" && tb === \"bigint\") {\n const naStr = na as string;\n const nbBig = nb as bigint;\n return `${nbBig.toString()}n` === naStr || nbBig.toString() === naStr.replace(/n$/, \"\");\n }\n return false;\n }\n\n if (ta === \"object\" && na !== null && nb !== null) {\n return stableJsonEqual(na, nb);\n }\n\n return false;\n}\n\n/** Drop patch keys whose values match the server row baseline. */\nexport function filterChangedPatchFields(\n patch: Record<string, unknown>,\n baseline: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(patch)) {\n if (!fieldValuesEqual(value, baseline?.[key])) out[key] = value;\n }\n return out;\n}\n","import { fieldValuesEqual } from \"./fieldValueEqual.ts\";\nimport type { ProposalFieldDiff, ProposalRecordFieldDiff } from \"./shareTypes.ts\";\n\nexport function computeProposalFieldDiff(\n baseline: Record<string, unknown> | null | undefined,\n fields: Record<string, unknown>,\n): ProposalFieldDiff[] {\n return Object.entries(fields)\n .filter(([field, after]) => !fieldValuesEqual(baseline?.[field], after))\n .map(([field, after]) => ({ field, before: baseline?.[field], after }));\n}\n\n/** Merge baseline + patch into the record a reviewer would see after accept. */\nexport function mergeProposedRecord(\n baseline: Record<string, unknown> | null | undefined,\n fields: Record<string, unknown>,\n op: \"insert\" | \"update\" | \"delete\",\n): Record<string, unknown> {\n if (op === \"delete\") return { ...(baseline ?? {}) };\n if (op === \"insert\") return { ...fields };\n return { ...(baseline ?? {}), ...fields };\n}\n\n/** Field names for full-record review: schema order when available, else baseline ∪ patch. */\nexport function proposalDiffFieldNames(\n tableFields: readonly { name: string }[] | undefined,\n baseline: Record<string, unknown> | null | undefined,\n fields: Record<string, unknown>,\n): string[] {\n if (tableFields && tableFields.length > 0) {\n return tableFields.map((field) => field.name);\n }\n const names = new Set<string>();\n for (const key of Object.keys(baseline ?? {})) {\n if (!key.startsWith(\"_\")) names.add(key);\n }\n for (const key of Object.keys(fields)) {\n if (!key.startsWith(\"_\")) names.add(key);\n }\n return [...names];\n}\n\n/**\n * Full-record field diffs in schema (or key) order, including unchanged fields.\n * `changed` mirrors patch membership / equality — same rules as computeProposalFieldDiff.\n */\nexport function computeProposalRecordDiff(\n fieldNames: readonly string[],\n baseline: Record<string, unknown> | null | undefined,\n fields: Record<string, unknown>,\n op: \"insert\" | \"update\" | \"delete\",\n): ProposalRecordFieldDiff[] {\n return fieldNames.map((field) => {\n const before = baseline?.[field];\n if (op === \"delete\") {\n return {\n field,\n before,\n after: undefined,\n changed: before !== undefined,\n };\n }\n const inPatch = Object.hasOwn(fields, field);\n const after = inPatch ? fields[field] : op === \"insert\" ? undefined : before;\n const changed = op === \"insert\" ? inPatch : inPatch && !fieldValuesEqual(before, fields[field]);\n return { field, before, after, changed };\n });\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Manifest } from "./schemaTypes.ts";
|
|
2
|
+
import type { ShareInviteKind, ShareLinkType, ShareOperation, ShareScope, ShareScopeConflictPatch, ShareWizardReferenceInput } from "./shareTypes.ts";
|
|
3
|
+
export declare function normalizeShareScopes(scopes: ShareScope[]): ShareScope[];
|
|
4
|
+
export declare function isReferenceOnlyScope(scope: ShareScope): boolean;
|
|
5
|
+
export declare function mergeReferenceScopes(scopes: ShareScope[], references: ShareWizardReferenceInput[]): ShareScope[];
|
|
6
|
+
export declare function isFormSubmissionScope(scope: ShareScope): boolean;
|
|
7
|
+
export declare function canSearchFormReferences(scope: ShareScope): boolean;
|
|
8
|
+
export declare function canCreateFormReferences(scope: ShareScope | undefined): boolean;
|
|
9
|
+
export declare function assertShareLinkTypeConsistency(type: ShareLinkType, scopes: ShareScope[], inviteKind?: ShareInviteKind): void;
|
|
10
|
+
export declare function canProposeOperation(scope: ShareScope | undefined, operation: ShareOperation): boolean;
|
|
11
|
+
export declare function canProposeInsert(scope: ShareScope | undefined): boolean;
|
|
12
|
+
export declare function eligibleReferencedParents(manifest: Manifest, scopes: ShareScope[], targetTable: string): ShareScope[];
|
|
13
|
+
export declare function assertShareOperation(scopes: ShareScope[], table: string, operation: ShareOperation, rowId?: string, fields?: string[]): ShareScope;
|
|
14
|
+
export declare function findShareScopeConflicts(scopes: ShareScope[], patches: ShareScopeConflictPatch[]): ShareScopeConflictPatch[];
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/proposePolicy.ts
|
|
2
|
+
function assertProposalAccess(accesses, target) {
|
|
3
|
+
const access = accesses.find((candidate) => candidate.table === target.table);
|
|
4
|
+
if (!access?.ops[target.operation]) {
|
|
5
|
+
throw new Error(`${target.operation} is not allowed for ${target.table}`);
|
|
6
|
+
}
|
|
7
|
+
if (access.recordIds && (!target.rowId || !access.recordIds.includes(target.rowId))) {
|
|
8
|
+
throw new Error(`Row is outside the allowed scope for ${target.table}`);
|
|
9
|
+
}
|
|
10
|
+
if (target.operation === "insert" && access.recordIds) {
|
|
11
|
+
throw new Error(`Insert is not allowed on a record-restricted ${target.table} scope`);
|
|
12
|
+
}
|
|
13
|
+
if (target.fields) {
|
|
14
|
+
const allowed = access.allowedFields ? new Set(access.allowedFields) : null;
|
|
15
|
+
const denied = target.fields.find(
|
|
16
|
+
(field) => field.startsWith("_") || allowed && !allowed.has(field)
|
|
17
|
+
);
|
|
18
|
+
if (denied) throw new Error(`Field ${denied} is not allowed for ${target.table}`);
|
|
19
|
+
}
|
|
20
|
+
return access;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// packages/@columnus/shared/src/refGraph.ts
|
|
24
|
+
function refEdgesForType(type) {
|
|
25
|
+
switch (type.kind) {
|
|
26
|
+
case "id":
|
|
27
|
+
return [{ field: "", targetTable: type.table, array: false }];
|
|
28
|
+
case "array":
|
|
29
|
+
if (type.element.kind === "id") {
|
|
30
|
+
return [{ field: "", targetTable: type.element.table, array: true }];
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
default:
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function refFieldsForTable(table) {
|
|
38
|
+
const edges = [];
|
|
39
|
+
for (const field of table.fields) {
|
|
40
|
+
for (const template of refEdgesForType(field.type)) {
|
|
41
|
+
edges.push({ ...template, field: field.name });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return edges.sort(
|
|
45
|
+
(a, b) => a.field.localeCompare(b.field) || a.targetTable.localeCompare(b.targetTable)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/@columnus/shared/src/shareScopes.ts
|
|
50
|
+
function inferLegacyPrimary(scope) {
|
|
51
|
+
const access = scope.formAccess;
|
|
52
|
+
return !(access && !access.submit && (access.searchReferences || access.createReferences));
|
|
53
|
+
}
|
|
54
|
+
function normalizeShareScopes(scopes) {
|
|
55
|
+
const tables = /* @__PURE__ */ new Set();
|
|
56
|
+
return scopes.map((scope) => {
|
|
57
|
+
const table = scope.table.trim();
|
|
58
|
+
if (!table) throw new Error("Every share scope needs a table");
|
|
59
|
+
if (tables.has(table)) throw new Error(`Duplicate share scope for ${table}`);
|
|
60
|
+
tables.add(table);
|
|
61
|
+
const recordIds = scope.recordIds ? [...new Set(scope.recordIds.map((id) => id.trim()).filter(Boolean))] : void 0;
|
|
62
|
+
const allowedFields = scope.allowedFields ? [...new Set(scope.allowedFields.map((field) => field.trim()).filter(Boolean))] : void 0;
|
|
63
|
+
if (scope.access === "referenced" && recordIds) {
|
|
64
|
+
throw new Error(`Referenced scope ${table} cannot specify recordIds`);
|
|
65
|
+
}
|
|
66
|
+
if (recordIds && scope.permission === "propose" && scope.ops.insert) {
|
|
67
|
+
throw new Error(`Record-picked scope ${table} cannot allow inserts`);
|
|
68
|
+
}
|
|
69
|
+
if (scope.allowCreate && (scope.access !== "referenced" || scope.permission !== "propose" || !scope.ops.insert)) {
|
|
70
|
+
throw new Error(`allowCreate requires referenced propose insert access for ${table}`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...scope,
|
|
74
|
+
table,
|
|
75
|
+
primary: scope.primary ?? inferLegacyPrimary(scope),
|
|
76
|
+
recordIds,
|
|
77
|
+
allowedFields,
|
|
78
|
+
ops: scope.permission === "view" ? { insert: false, update: false, delete: false } : { ...scope.ops }
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function isReferenceOnlyScope(scope) {
|
|
83
|
+
return scope.primary === false;
|
|
84
|
+
}
|
|
85
|
+
function mergeReferenceScopes(scopes, references) {
|
|
86
|
+
const next = normalizeShareScopes(scopes);
|
|
87
|
+
for (const reference of references) {
|
|
88
|
+
const existing = next.find((scope) => scope.table === reference.table.trim());
|
|
89
|
+
const formAccess = {
|
|
90
|
+
submit: existing?.formAccess?.submit ?? false,
|
|
91
|
+
searchReferences: (existing?.formAccess?.searchReferences ?? false) || reference.searchReferences,
|
|
92
|
+
createReferences: (existing?.formAccess?.createReferences ?? false) || reference.createReferences
|
|
93
|
+
};
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.formAccess = formAccess;
|
|
96
|
+
if (formAccess.createReferences && !existing.primary) {
|
|
97
|
+
existing.permission = "propose";
|
|
98
|
+
existing.ops = { insert: true, update: false, delete: false };
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
next.push({
|
|
103
|
+
table: reference.table,
|
|
104
|
+
primary: false,
|
|
105
|
+
access: "full",
|
|
106
|
+
permission: formAccess.createReferences ? "propose" : "view",
|
|
107
|
+
ops: formAccess.createReferences ? { insert: true, update: false, delete: false } : { insert: false, update: false, delete: false },
|
|
108
|
+
formAccess
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
function isInsertOnlyScope(scope) {
|
|
114
|
+
return scope.permission === "propose" && scope.ops.insert && !scope.ops.update && !scope.ops.delete;
|
|
115
|
+
}
|
|
116
|
+
function isFormSubmissionScope(scope) {
|
|
117
|
+
return scope.formAccess?.submit === true;
|
|
118
|
+
}
|
|
119
|
+
function canSearchFormReferences(scope) {
|
|
120
|
+
return scope.formAccess?.searchReferences === true;
|
|
121
|
+
}
|
|
122
|
+
function canCreateFormReferences(scope) {
|
|
123
|
+
return scope?.formAccess?.createReferences === true;
|
|
124
|
+
}
|
|
125
|
+
function assertShareLinkTypeConsistency(type, scopes, inviteKind) {
|
|
126
|
+
const normalized = normalizeShareScopes(scopes);
|
|
127
|
+
if (normalized.length === 0) throw new Error("Add at least one table scope");
|
|
128
|
+
if (type === "propose" && inviteKind === "public") {
|
|
129
|
+
throw new Error("Proposal shares require a recipient invite");
|
|
130
|
+
}
|
|
131
|
+
for (const scope of normalized) {
|
|
132
|
+
if (type === "view" && scope.permission !== "view") {
|
|
133
|
+
throw new Error(`View shares require read-only access for ${scope.table}`);
|
|
134
|
+
}
|
|
135
|
+
if (type === "propose" && scope.permission !== "propose" && (!isReferenceOnlyScope(scope) || scope.permission !== "view")) {
|
|
136
|
+
throw new Error(`Proposal shares require proposal access for ${scope.table}`);
|
|
137
|
+
}
|
|
138
|
+
if (type === "propose" && scope.formAccess?.submit) {
|
|
139
|
+
throw new Error(`Proposal shares cannot use form submission scopes for ${scope.table}`);
|
|
140
|
+
}
|
|
141
|
+
if (type === "propose" && isReferenceOnlyScope(scope)) {
|
|
142
|
+
const formAccess = scope.formAccess;
|
|
143
|
+
if (!formAccess) throw new Error(`Reference scope ${scope.table} needs reference access`);
|
|
144
|
+
if (scope.access !== "full") {
|
|
145
|
+
throw new Error(`Reference picker scopes must use full-table access for ${scope.table}`);
|
|
146
|
+
}
|
|
147
|
+
if (formAccess.createReferences && !isInsertOnlyScope(scope)) {
|
|
148
|
+
throw new Error(`Reference creation requires insert-only access for ${scope.table}`);
|
|
149
|
+
}
|
|
150
|
+
if (!formAccess.createReferences && scope.permission !== "view") {
|
|
151
|
+
throw new Error(`Read-only reference picker access is required for ${scope.table}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (type === "form") assertFormScope(scope);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function assertFormScope(scope) {
|
|
158
|
+
const access = scope.formAccess;
|
|
159
|
+
if (!access) throw new Error(`Form shares require explicit form access for ${scope.table}`);
|
|
160
|
+
if (!access.submit && !access.searchReferences && !access.createReferences) {
|
|
161
|
+
throw new Error(`Form scope ${scope.table} does not grant any capability`);
|
|
162
|
+
}
|
|
163
|
+
if (scope.access !== "full")
|
|
164
|
+
throw new Error(`Form scopes must use full-table access for ${scope.table}`);
|
|
165
|
+
if ((access.submit || access.createReferences) && !isInsertOnlyScope(scope)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Form submission and reference creation require insert-only access for ${scope.table}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (!access.submit && !access.createReferences && scope.permission !== "view") {
|
|
171
|
+
throw new Error(`Read-only form reference access is required for ${scope.table}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function canProposeOperation(scope, operation) {
|
|
175
|
+
if (scope?.permission !== "propose" || !scope.ops[operation]) return false;
|
|
176
|
+
return operation !== "insert" || scope.access !== "referenced" || Boolean(scope.allowCreate);
|
|
177
|
+
}
|
|
178
|
+
function canProposeInsert(scope) {
|
|
179
|
+
return canProposeOperation(scope, "insert");
|
|
180
|
+
}
|
|
181
|
+
function eligibleReferencedParents(manifest, scopes, targetTable) {
|
|
182
|
+
return scopes.filter((scope) => {
|
|
183
|
+
const table = manifest.tables.find((candidate) => candidate.name === scope.table);
|
|
184
|
+
return Boolean(
|
|
185
|
+
table && table.name !== targetTable && refFieldsForTable(table).some((edge) => edge.targetTable === targetTable)
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function assertShareOperation(scopes, table, operation, rowId, fields) {
|
|
190
|
+
const scope = scopes.find((candidate) => candidate.table === table);
|
|
191
|
+
if (scope?.permission !== "propose") throw new Error(`${operation} is not allowed for ${table}`);
|
|
192
|
+
const access = {
|
|
193
|
+
table: scope.table,
|
|
194
|
+
ops: scope.ops,
|
|
195
|
+
recordIds: scope.recordIds,
|
|
196
|
+
allowedFields: scope.allowedFields
|
|
197
|
+
};
|
|
198
|
+
assertProposalAccess([access], { table, operation, rowId, fields });
|
|
199
|
+
if (operation === "insert" && scope.access === "referenced" && !scope.allowCreate) {
|
|
200
|
+
throw new Error(`Creating referenced ${table} rows is not allowed`);
|
|
201
|
+
}
|
|
202
|
+
return scope;
|
|
203
|
+
}
|
|
204
|
+
function findShareScopeConflicts(scopes, patches) {
|
|
205
|
+
return patches.filter((patch) => {
|
|
206
|
+
try {
|
|
207
|
+
assertShareOperation(
|
|
208
|
+
scopes,
|
|
209
|
+
patch.table,
|
|
210
|
+
patch.op,
|
|
211
|
+
patch.rowId.startsWith("new:") ? void 0 : patch.rowId,
|
|
212
|
+
Object.keys(patch.fields)
|
|
213
|
+
);
|
|
214
|
+
return false;
|
|
215
|
+
} catch {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { assertShareLinkTypeConsistency, assertShareOperation, canCreateFormReferences, canProposeInsert, canProposeOperation, canSearchFormReferences, eligibleReferencedParents, findShareScopeConflicts, isFormSubmissionScope, isReferenceOnlyScope, mergeReferenceScopes, normalizeShareScopes };
|
|
222
|
+
//# sourceMappingURL=shareScopes.js.map
|
|
223
|
+
//# sourceMappingURL=shareScopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proposePolicy.ts","../src/refGraph.ts","../src/shareScopes.ts"],"names":[],"mappings":"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","file":"shareScopes.js","sourcesContent":["export type ProposalOperation = \"insert\" | \"update\" | \"delete\";\n\nexport type ProposalAccess = {\n table: string;\n ops: Record<ProposalOperation, boolean>;\n recordIds?: string[];\n allowedFields?: string[];\n};\n\nexport type ProposalTarget = {\n table: string;\n operation: ProposalOperation;\n rowId?: string;\n fields?: string[];\n};\n\n/** Enforce the proposal rules shared by API profiles and share scopes. */\nexport function assertProposalAccess(\n accesses: readonly ProposalAccess[],\n target: ProposalTarget,\n): ProposalAccess {\n const access = accesses.find((candidate) => candidate.table === target.table);\n if (!access?.ops[target.operation]) {\n throw new Error(`${target.operation} is not allowed for ${target.table}`);\n }\n if (access.recordIds && (!target.rowId || !access.recordIds.includes(target.rowId))) {\n throw new Error(`Row is outside the allowed scope for ${target.table}`);\n }\n if (target.operation === \"insert\" && access.recordIds) {\n throw new Error(`Insert is not allowed on a record-restricted ${target.table} scope`);\n }\n if (target.fields) {\n const allowed = access.allowedFields ? new Set(access.allowedFields) : null;\n const denied = target.fields.find(\n (field) => field.startsWith(\"_\") || (allowed && !allowed.has(field)),\n );\n if (denied) throw new Error(`Field ${denied} is not allowed for ${target.table}`);\n }\n return access;\n}\n\n/** Strip system and disallowed fields before persisting a proposal patch. */\nexport function pickProposalFields(\n fields: Record<string, unknown>,\n allowedFields?: string[],\n): Record<string, unknown> {\n const allowed = allowedFields ? new Set(allowedFields) : null;\n return Object.fromEntries(\n Object.entries(fields).filter(\n ([field]) => !field.startsWith(\"_\") && (!allowed || allowed.has(field)),\n ),\n );\n}\n","import type { FieldType, Manifest, Table } from \"./schemaTypes.ts\";\n\nexport type RefFieldEdge = {\n field: string;\n targetTable: string;\n array: boolean;\n};\n\nfunction refEdgesForType(type: FieldType): RefFieldEdge[] {\n switch (type.kind) {\n case \"id\":\n return [{ field: \"\", targetTable: type.table, array: false }];\n case \"array\":\n if (type.element.kind === \"id\") {\n return [{ field: \"\", targetTable: type.element.table, array: true }];\n }\n return [];\n default:\n return [];\n }\n}\n\nfunction collectTargetIdsFromValue(\n value: unknown,\n edge: RefFieldEdge,\n refs: Map<string, Set<string>>,\n): void {\n if (edge.array) {\n if (!Array.isArray(value)) return;\n for (const item of value) {\n if (typeof item === \"string\" && item.length > 0) addRef(refs, edge.targetTable, item);\n }\n return;\n }\n if (typeof value === \"string\" && value.length > 0) addRef(refs, edge.targetTable, value);\n}\n\nexport function collectReferencedIds(\n table: Table,\n rows: Record<string, unknown>[],\n): Map<string, Set<string>> {\n const refs = new Map<string, Set<string>>();\n for (const edge of refFieldsForTable(table)) {\n for (const row of rows) {\n collectTargetIdsFromValue(row[edge.field], edge, refs);\n }\n }\n return refs;\n}\n\nexport function collectIdsFromField(\n rows: Record<string, unknown>[],\n fieldName: string,\n type: FieldType,\n refs: Map<string, Set<string>>,\n): void {\n for (const template of refEdgesForType(type)) {\n const edge: RefFieldEdge = { ...template, field: fieldName };\n for (const row of rows) {\n collectTargetIdsFromValue(row[fieldName], edge, refs);\n }\n }\n}\n\n/** Collect referenced document ids from raw field values (selection chips, formula bar). */\nexport function collectIdsFromValues(\n values: readonly unknown[],\n type: FieldType,\n refs: Map<string, Set<string>>,\n): void {\n for (const template of refEdgesForType(type)) {\n for (const value of values) {\n collectTargetIdsFromValue(value, template, refs);\n }\n }\n}\n\nexport function addRef(refs: Map<string, Set<string>>, table: string, id: string): void {\n const set = refs.get(table) ?? new Set<string>();\n set.add(id);\n refs.set(table, set);\n}\n\n/** Manifest `id` / `id[]` ref topology for one table (field names + targets). */\nexport function refFieldsForTable(table: Table): RefFieldEdge[] {\n const edges: RefFieldEdge[] = [];\n for (const field of table.fields) {\n for (const template of refEdgesForType(field.type)) {\n edges.push({ ...template, field: field.name });\n }\n }\n return edges.sort(\n (a, b) => a.field.localeCompare(b.field) || a.targetTable.localeCompare(b.targetTable),\n );\n}\n\n/** Tables that have at least one outgoing `id` / `id[]` field. */\nexport function tablesWithOutgoingRefs(manifest: Manifest): string[] {\n return manifest.tables\n .filter((table) => refFieldsForTable(table).length > 0)\n .map((table) => table.name)\n .sort((a, b) => a.localeCompare(b));\n}\n\nfunction tableRefFingerprint(table: Table): string {\n return JSON.stringify(refFieldsForTable(table));\n}\n\n/** Stable fingerprint of manifest ref topology (ignores labels, indexes, display templates). */\nexport function computeRefGraphFingerprint(manifest: Manifest): string {\n const tables = [...manifest.tables]\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((table) => ({ name: table.name, refs: refFieldsForTable(table) }));\n return JSON.stringify(tables);\n}\n\n/** Tables whose ref-field topology changed between two manifests. */\nexport function listTablesWithRefGraphChanges(before: Manifest, after: Manifest): string[] {\n const beforeMap = new Map(before.tables.map((t) => [t.name, tableRefFingerprint(t)]));\n const afterMap = new Map(after.tables.map((t) => [t.name, tableRefFingerprint(t)]));\n const changed = new Set<string>();\n for (const [name, fp] of afterMap) {\n if (beforeMap.get(name) !== fp) changed.add(name);\n }\n for (const table of before.tables) {\n if (!afterMap.has(table.name) && refFieldsForTable(table).length > 0) {\n changed.add(table.name);\n }\n }\n return [...changed].sort((a, b) => a.localeCompare(b));\n}\n\n/** Whether a field type can hold outgoing `id` / `id[]` references. */\nexport function fieldTypeHasOutgoingRefs(type: FieldType): boolean {\n return refEdgesForType(type).length > 0;\n}\n\n/** Flatten `collectReferencedIds` output to unique target document ids. */\nexport function referencedTargetIds(refs: Map<string, Set<string>>): Set<string> {\n const out = new Set<string>();\n for (const ids of refs.values()) {\n for (const id of ids) out.add(id);\n }\n return out;\n}\n","import { assertProposalAccess, type ProposalAccess } from \"./proposePolicy.ts\";\nimport { refFieldsForTable } from \"./refGraph.ts\";\nimport type { Manifest } from \"./schemaTypes.ts\";\nimport type {\n ShareInviteKind,\n ShareLinkType,\n ShareOperation,\n ShareScope,\n ShareScopeConflictPatch,\n ShareWizardReferenceInput,\n} from \"./shareTypes.ts\";\n\nfunction inferLegacyPrimary(scope: ShareScope): boolean {\n const access = scope.formAccess;\n return !(access && !access.submit && (access.searchReferences || access.createReferences));\n}\n\nexport function normalizeShareScopes(scopes: ShareScope[]): ShareScope[] {\n const tables = new Set<string>();\n return scopes.map((scope) => {\n const table = scope.table.trim();\n if (!table) throw new Error(\"Every share scope needs a table\");\n if (tables.has(table)) throw new Error(`Duplicate share scope for ${table}`);\n tables.add(table);\n\n const recordIds = scope.recordIds\n ? [...new Set(scope.recordIds.map((id) => id.trim()).filter(Boolean))]\n : undefined;\n const allowedFields = scope.allowedFields\n ? [...new Set(scope.allowedFields.map((field) => field.trim()).filter(Boolean))]\n : undefined;\n\n if (scope.access === \"referenced\" && recordIds) {\n throw new Error(`Referenced scope ${table} cannot specify recordIds`);\n }\n if (recordIds && scope.permission === \"propose\" && scope.ops.insert) {\n throw new Error(`Record-picked scope ${table} cannot allow inserts`);\n }\n if (\n scope.allowCreate &&\n (scope.access !== \"referenced\" || scope.permission !== \"propose\" || !scope.ops.insert)\n ) {\n throw new Error(`allowCreate requires referenced propose insert access for ${table}`);\n }\n\n return {\n ...scope,\n table,\n primary: scope.primary ?? inferLegacyPrimary(scope),\n recordIds,\n allowedFields,\n ops:\n scope.permission === \"view\"\n ? { insert: false, update: false, delete: false }\n : { ...scope.ops },\n };\n });\n}\n\nexport function isReferenceOnlyScope(scope: ShareScope): boolean {\n return scope.primary === false;\n}\n\nexport function mergeReferenceScopes(\n scopes: ShareScope[],\n references: ShareWizardReferenceInput[],\n): ShareScope[] {\n const next = normalizeShareScopes(scopes);\n for (const reference of references) {\n const existing = next.find((scope) => scope.table === reference.table.trim());\n const formAccess = {\n submit: existing?.formAccess?.submit ?? false,\n searchReferences:\n (existing?.formAccess?.searchReferences ?? false) || reference.searchReferences,\n createReferences:\n (existing?.formAccess?.createReferences ?? false) || reference.createReferences,\n };\n if (existing) {\n existing.formAccess = formAccess;\n if (formAccess.createReferences && !existing.primary) {\n existing.permission = \"propose\";\n existing.ops = { insert: true, update: false, delete: false };\n }\n continue;\n }\n next.push({\n table: reference.table,\n primary: false,\n access: \"full\",\n permission: formAccess.createReferences ? \"propose\" : \"view\",\n ops: formAccess.createReferences\n ? { insert: true, update: false, delete: false }\n : { insert: false, update: false, delete: false },\n formAccess,\n });\n }\n return next;\n}\n\nfunction isInsertOnlyScope(scope: ShareScope): boolean {\n return (\n scope.permission === \"propose\" && scope.ops.insert && !scope.ops.update && !scope.ops.delete\n );\n}\n\nexport function isFormSubmissionScope(scope: ShareScope): boolean {\n return scope.formAccess?.submit === true;\n}\n\nexport function canSearchFormReferences(scope: ShareScope): boolean {\n return scope.formAccess?.searchReferences === true;\n}\n\nexport function canCreateFormReferences(scope: ShareScope | undefined): boolean {\n return scope?.formAccess?.createReferences === true;\n}\n\nexport function assertShareLinkTypeConsistency(\n type: ShareLinkType,\n scopes: ShareScope[],\n inviteKind?: ShareInviteKind,\n): void {\n const normalized = normalizeShareScopes(scopes);\n if (normalized.length === 0) throw new Error(\"Add at least one table scope\");\n if (type === \"propose\" && inviteKind === \"public\") {\n throw new Error(\"Proposal shares require a recipient invite\");\n }\n for (const scope of normalized) {\n if (type === \"view\" && scope.permission !== \"view\") {\n throw new Error(`View shares require read-only access for ${scope.table}`);\n }\n if (\n type === \"propose\" &&\n scope.permission !== \"propose\" &&\n (!isReferenceOnlyScope(scope) || scope.permission !== \"view\")\n ) {\n throw new Error(`Proposal shares require proposal access for ${scope.table}`);\n }\n if (type === \"propose\" && scope.formAccess?.submit) {\n throw new Error(`Proposal shares cannot use form submission scopes for ${scope.table}`);\n }\n if (type === \"propose\" && isReferenceOnlyScope(scope)) {\n const formAccess = scope.formAccess;\n if (!formAccess) throw new Error(`Reference scope ${scope.table} needs reference access`);\n if (scope.access !== \"full\") {\n throw new Error(`Reference picker scopes must use full-table access for ${scope.table}`);\n }\n if (formAccess.createReferences && !isInsertOnlyScope(scope)) {\n throw new Error(`Reference creation requires insert-only access for ${scope.table}`);\n }\n if (!formAccess.createReferences && scope.permission !== \"view\") {\n throw new Error(`Read-only reference picker access is required for ${scope.table}`);\n }\n }\n if (type === \"form\") assertFormScope(scope);\n }\n}\n\nfunction assertFormScope(scope: ShareScope): void {\n const access = scope.formAccess;\n if (!access) throw new Error(`Form shares require explicit form access for ${scope.table}`);\n if (!access.submit && !access.searchReferences && !access.createReferences) {\n throw new Error(`Form scope ${scope.table} does not grant any capability`);\n }\n if (scope.access !== \"full\")\n throw new Error(`Form scopes must use full-table access for ${scope.table}`);\n if ((access.submit || access.createReferences) && !isInsertOnlyScope(scope)) {\n throw new Error(\n `Form submission and reference creation require insert-only access for ${scope.table}`,\n );\n }\n if (!access.submit && !access.createReferences && scope.permission !== \"view\") {\n throw new Error(`Read-only form reference access is required for ${scope.table}`);\n }\n}\n\nexport function canProposeOperation(\n scope: ShareScope | undefined,\n operation: ShareOperation,\n): boolean {\n if (scope?.permission !== \"propose\" || !scope.ops[operation]) return false;\n return operation !== \"insert\" || scope.access !== \"referenced\" || Boolean(scope.allowCreate);\n}\n\nexport function canProposeInsert(scope: ShareScope | undefined): boolean {\n return canProposeOperation(scope, \"insert\");\n}\n\nexport function eligibleReferencedParents(\n manifest: Manifest,\n scopes: ShareScope[],\n targetTable: string,\n): ShareScope[] {\n return scopes.filter((scope) => {\n const table = manifest.tables.find((candidate) => candidate.name === scope.table);\n return Boolean(\n table &&\n table.name !== targetTable &&\n refFieldsForTable(table).some((edge) => edge.targetTable === targetTable),\n );\n });\n}\n\nexport function assertShareOperation(\n scopes: ShareScope[],\n table: string,\n operation: ShareOperation,\n rowId?: string,\n fields?: string[],\n): ShareScope {\n const scope = scopes.find((candidate) => candidate.table === table);\n if (scope?.permission !== \"propose\") throw new Error(`${operation} is not allowed for ${table}`);\n const access = {\n table: scope.table,\n ops: scope.ops,\n recordIds: scope.recordIds,\n allowedFields: scope.allowedFields,\n } satisfies ProposalAccess;\n assertProposalAccess([access], { table, operation, rowId, fields });\n if (operation === \"insert\" && scope.access === \"referenced\" && !scope.allowCreate) {\n throw new Error(`Creating referenced ${table} rows is not allowed`);\n }\n return scope;\n}\n\nexport function findShareScopeConflicts(\n scopes: ShareScope[],\n patches: ShareScopeConflictPatch[],\n): ShareScopeConflictPatch[] {\n return patches.filter((patch) => {\n try {\n assertShareOperation(\n scopes,\n patch.table,\n patch.op,\n patch.rowId.startsWith(\"new:\") ? undefined : patch.rowId,\n Object.keys(patch.fields),\n );\n return false;\n } catch {\n return true;\n }\n });\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption for creator-only guest-share tokens. Ciphertexts use AES-256-GCM
|
|
3
|
+
* with a random 12-byte IV and are safe to store with the invite record.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SHARE_TOKEN_ENCRYPTION_KEY_ENV = "SHARE_TOKEN_ENCRYPTION_KEY";
|
|
6
|
+
/** Parse a 32-byte AES key encoded as standard or URL-safe base64. */
|
|
7
|
+
export declare function parseShareTokenEncryptionKey(raw: string): Uint8Array;
|
|
8
|
+
export declare function encryptShareToken(plaintext: string, keyBytes: Uint8Array): Promise<string>;
|
|
9
|
+
export declare function decryptShareToken(ciphertext: string, keyBytes: Uint8Array): Promise<string>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/shareTokenCrypto.ts
|
|
2
|
+
var SHARE_TOKEN_ENCRYPTION_KEY_ENV = "SHARE_TOKEN_ENCRYPTION_KEY";
|
|
3
|
+
var IV_BYTES = 12;
|
|
4
|
+
var KEY_BYTES = 32;
|
|
5
|
+
var VERSION = 1;
|
|
6
|
+
function base64UrlEncode(bytes) {
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
9
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
10
|
+
}
|
|
11
|
+
function base64UrlDecode(value) {
|
|
12
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
13
|
+
const padLength = (4 - padded.length % 4) % 4;
|
|
14
|
+
const binary = atob(padded + "=".repeat(padLength));
|
|
15
|
+
const bytes = new Uint8Array(binary.length);
|
|
16
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
function parseShareTokenEncryptionKey(raw) {
|
|
20
|
+
const value = raw.trim();
|
|
21
|
+
if (!value) throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} is empty`);
|
|
22
|
+
let key;
|
|
23
|
+
try {
|
|
24
|
+
key = base64UrlDecode(value);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} must be a base64-encoded 32-byte key`);
|
|
27
|
+
}
|
|
28
|
+
if (key.byteLength !== KEY_BYTES) {
|
|
29
|
+
throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} must decode to ${KEY_BYTES} bytes`);
|
|
30
|
+
}
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
function asArrayBufferView(bytes) {
|
|
34
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
35
|
+
copy.set(bytes);
|
|
36
|
+
return copy;
|
|
37
|
+
}
|
|
38
|
+
async function importKey(keyBytes) {
|
|
39
|
+
if (keyBytes.byteLength !== KEY_BYTES) throw new Error("Share token key must be 32 bytes");
|
|
40
|
+
return await crypto.subtle.importKey("raw", asArrayBufferView(keyBytes), "AES-GCM", false, [
|
|
41
|
+
"encrypt",
|
|
42
|
+
"decrypt"
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
async function encryptShareToken(plaintext, keyBytes) {
|
|
46
|
+
const token = plaintext.trim();
|
|
47
|
+
if (!token) throw new Error("Refusing to encrypt an empty share token");
|
|
48
|
+
const iv = new Uint8Array(IV_BYTES);
|
|
49
|
+
crypto.getRandomValues(iv);
|
|
50
|
+
const encrypted = new Uint8Array(
|
|
51
|
+
await crypto.subtle.encrypt(
|
|
52
|
+
{ name: "AES-GCM", iv: asArrayBufferView(iv) },
|
|
53
|
+
await importKey(keyBytes),
|
|
54
|
+
new TextEncoder().encode(token)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
const packed = new Uint8Array(1 + IV_BYTES + encrypted.byteLength);
|
|
58
|
+
packed[0] = VERSION;
|
|
59
|
+
packed.set(iv, 1);
|
|
60
|
+
packed.set(encrypted, 1 + IV_BYTES);
|
|
61
|
+
return base64UrlEncode(packed);
|
|
62
|
+
}
|
|
63
|
+
async function decryptShareToken(ciphertext, keyBytes) {
|
|
64
|
+
let packed;
|
|
65
|
+
try {
|
|
66
|
+
packed = base64UrlDecode(ciphertext.trim());
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error("Creator token ciphertext is not valid base64url");
|
|
69
|
+
}
|
|
70
|
+
if (packed.byteLength < 1 + IV_BYTES + 16)
|
|
71
|
+
throw new Error("Creator token ciphertext is too short");
|
|
72
|
+
if (packed[0] !== VERSION) throw new Error("Unsupported creator token ciphertext version");
|
|
73
|
+
try {
|
|
74
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
75
|
+
{ name: "AES-GCM", iv: asArrayBufferView(packed.subarray(1, 1 + IV_BYTES)) },
|
|
76
|
+
await importKey(keyBytes),
|
|
77
|
+
asArrayBufferView(packed.subarray(1 + IV_BYTES))
|
|
78
|
+
);
|
|
79
|
+
return new TextDecoder().decode(plaintext);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error("Failed to decrypt creator token");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { SHARE_TOKEN_ENCRYPTION_KEY_ENV, decryptShareToken, encryptShareToken, parseShareTokenEncryptionKey };
|
|
86
|
+
//# sourceMappingURL=shareTokenCrypto.js.map
|
|
87
|
+
//# sourceMappingURL=shareTokenCrypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shareTokenCrypto.ts"],"names":[],"mappings":"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","file":"shareTokenCrypto.js","sourcesContent":["/**\n * Encryption for creator-only guest-share tokens. Ciphertexts use AES-256-GCM\n * with a random 12-byte IV and are safe to store with the invite record.\n */\n\nexport const SHARE_TOKEN_ENCRYPTION_KEY_ENV = \"SHARE_TOKEN_ENCRYPTION_KEY\";\n\nconst IV_BYTES = 12;\nconst KEY_BYTES = 32;\nconst VERSION = 1;\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 base64UrlDecode(value: string): Uint8Array {\n const padded = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padLength = (4 - (padded.length % 4)) % 4;\n const binary = atob(padded + \"=\".repeat(padLength));\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\n/** Parse a 32-byte AES key encoded as standard or URL-safe base64. */\nexport function parseShareTokenEncryptionKey(raw: string): Uint8Array {\n const value = raw.trim();\n if (!value) throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} is empty`);\n let key: Uint8Array;\n try {\n key = base64UrlDecode(value);\n } catch {\n throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} must be a base64-encoded 32-byte key`);\n }\n if (key.byteLength !== KEY_BYTES) {\n throw new Error(`${SHARE_TOKEN_ENCRYPTION_KEY_ENV} must decode to ${KEY_BYTES} bytes`);\n }\n return key;\n}\n\nfunction asArrayBufferView(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy;\n}\n\nasync function importKey(keyBytes: Uint8Array) {\n if (keyBytes.byteLength !== KEY_BYTES) throw new Error(\"Share token key must be 32 bytes\");\n return await crypto.subtle.importKey(\"raw\", asArrayBufferView(keyBytes), \"AES-GCM\", false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\nexport async function encryptShareToken(plaintext: string, keyBytes: Uint8Array): Promise<string> {\n const token = plaintext.trim();\n if (!token) throw new Error(\"Refusing to encrypt an empty share token\");\n const iv = new Uint8Array(IV_BYTES);\n crypto.getRandomValues(iv);\n const encrypted = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv: asArrayBufferView(iv) },\n await importKey(keyBytes),\n new TextEncoder().encode(token),\n ),\n );\n const packed = new Uint8Array(1 + IV_BYTES + encrypted.byteLength);\n packed[0] = VERSION;\n packed.set(iv, 1);\n packed.set(encrypted, 1 + IV_BYTES);\n return base64UrlEncode(packed);\n}\n\nexport async function decryptShareToken(ciphertext: string, keyBytes: Uint8Array): Promise<string> {\n let packed: Uint8Array;\n try {\n packed = base64UrlDecode(ciphertext.trim());\n } catch {\n throw new Error(\"Creator token ciphertext is not valid base64url\");\n }\n if (packed.byteLength < 1 + IV_BYTES + 16)\n throw new Error(\"Creator token ciphertext is too short\");\n if (packed[0] !== VERSION) throw new Error(\"Unsupported creator token ciphertext version\");\n try {\n const plaintext = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv: asArrayBufferView(packed.subarray(1, 1 + IV_BYTES)) },\n await importKey(keyBytes),\n asArrayBufferView(packed.subarray(1 + IV_BYTES)),\n );\n return new TextDecoder().decode(plaintext);\n } catch {\n throw new Error(\"Failed to decrypt creator token\");\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ShareGuestViewMode, ShareScope } from "./shareTypes.ts";
|
|
2
|
+
export declare const SHARE_TOKEN_PREFIX = "shr_";
|
|
3
|
+
export declare function inferDefaultGuestView(scopes: ShareScope[]): ShareGuestViewMode | undefined;
|
|
4
|
+
export declare function generateShareToken(): string;
|
|
5
|
+
export declare function hashShareToken(token: string): Promise<string>;
|
|
6
|
+
export declare function shareTokenPrefix(token: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/shareTokens.ts
|
|
2
|
+
var SHARE_TOKEN_PREFIX = "shr_";
|
|
3
|
+
function inferDefaultGuestView(scopes) {
|
|
4
|
+
if (scopes.length !== 1) return void 0;
|
|
5
|
+
return scopes[0]?.recordIds ? "detail" : "grid";
|
|
6
|
+
}
|
|
7
|
+
function generateShareToken() {
|
|
8
|
+
const bytes = new Uint8Array(24);
|
|
9
|
+
crypto.getRandomValues(bytes);
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
12
|
+
const body = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
13
|
+
return `${SHARE_TOKEN_PREFIX}${body}`;
|
|
14
|
+
}
|
|
15
|
+
async function hashShareToken(token) {
|
|
16
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token.trim()));
|
|
17
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
}
|
|
19
|
+
function shareTokenPrefix(token) {
|
|
20
|
+
const trimmed = token.trim();
|
|
21
|
+
return `${trimmed.slice(0, SHARE_TOKEN_PREFIX.length + 7)}\u2026`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { SHARE_TOKEN_PREFIX, generateShareToken, hashShareToken, inferDefaultGuestView, shareTokenPrefix };
|
|
25
|
+
//# sourceMappingURL=shareTokens.js.map
|
|
26
|
+
//# sourceMappingURL=shareTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shareTokens.ts"],"names":[],"mappings":";AAEO,IAAM,kBAAA,GAAqB;AAE3B,SAAS,sBAAsB,MAAA,EAAsD;AAC1F,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAChC,EAAA,OAAO,MAAA,CAAO,CAAC,CAAA,EAAG,SAAA,GAAY,QAAA,GAAW,MAAA;AAC3C;AAEO,SAAS,kBAAA,GAA6B;AAC3C,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAM,CAAA,CAAE,QAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACpF,EAAA,OAAO,CAAA,EAAG,kBAAkB,CAAA,EAAG,IAAI,CAAA,CAAA;AACrC;AAEA,eAAsB,eAAe,KAAA,EAAgC;AACnE,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAA,CAAM,IAAA,EAAM,CAAC,CAAA;AAC3F,EAAA,OAAO,CAAC,GAAG,IAAI,UAAA,CAAW,MAAM,CAAC,CAAA,CAAE,IAAI,CAAC,IAAA,KAAS,KAAK,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAC9F;AAEO,SAAS,iBAAiB,KAAA,EAAuB;AACtD,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,OAAO,GAAG,OAAA,CAAQ,KAAA,CAAM,GAAG,kBAAA,CAAmB,MAAA,GAAS,CAAC,CAAC,CAAA,MAAA,CAAA;AAC3D","file":"shareTokens.js","sourcesContent":["import type { ShareGuestViewMode, ShareScope } from \"./shareTypes.ts\";\n\nexport const SHARE_TOKEN_PREFIX = \"shr_\";\n\nexport function inferDefaultGuestView(scopes: ShareScope[]): ShareGuestViewMode | undefined {\n if (scopes.length !== 1) return undefined;\n return scopes[0]?.recordIds ? \"detail\" : \"grid\";\n}\n\nexport function generateShareToken(): string {\n const bytes = new Uint8Array(24);\n crypto.getRandomValues(bytes);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n const body = btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/g, \"\");\n return `${SHARE_TOKEN_PREFIX}${body}`;\n}\n\nexport async function hashShareToken(token: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token.trim()));\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport function shareTokenPrefix(token: string): string {\n const trimmed = token.trim();\n return `${trimmed.slice(0, SHARE_TOKEN_PREFIX.length + 7)}…`;\n}\n"]}
|