@kumwe/studio-core 0.1.0-alpha.7 → 0.1.0-beta.2
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/README.md +87 -4
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/authentication-lifetime.d.ts +16 -0
- package/dist/authentication-lifetime.d.ts.map +1 -0
- package/dist/authentication-lifetime.js +1 -0
- package/dist/binding-projection.d.ts +47 -0
- package/dist/binding-projection.d.ts.map +1 -0
- package/dist/binding-projection.js +1 -0
- package/dist/canonical.js +1 -100
- package/dist/clone.js +1 -4
- package/dist/commands.d.ts +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -799
- package/dist/contextual-session.d.ts +112 -0
- package/dist/contextual-session.d.ts.map +1 -0
- package/dist/contextual-session.js +1 -0
- package/dist/contributions.d.ts +63 -2
- package/dist/contributions.d.ts.map +1 -1
- package/dist/contributions.js +1 -302
- package/dist/deployment-validation.d.ts +10 -0
- package/dist/deployment-validation.d.ts.map +1 -0
- package/dist/deployment-validation.js +1 -0
- package/dist/entry-commands.js +1 -36
- package/dist/extension-sdk.d.ts +12 -6
- package/dist/extension-sdk.d.ts.map +1 -1
- package/dist/extension-sdk.js +1 -188
- package/dist/history.d.ts +12 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +1 -63
- package/dist/host-session.d.ts +93 -0
- package/dist/host-session.d.ts.map +1 -0
- package/dist/host-session.js +1 -0
- package/dist/http-authoring-contract.d.ts +29 -0
- package/dist/http-authoring-contract.d.ts.map +1 -0
- package/dist/http-authoring-contract.js +1 -0
- package/dist/http-host-adapter.d.ts +173 -0
- package/dist/http-host-adapter.d.ts.map +1 -0
- package/dist/http-host-adapter.js +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -18
- package/dist/layout.d.ts +66 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +1 -0
- package/dist/migrations.js +1 -83
- package/dist/model-commands.js +1 -21
- package/dist/modes.js +1 -280
- package/dist/negotiation.js +1 -65
- package/dist/production-values.d.ts +12 -0
- package/dist/production-values.d.ts.map +1 -0
- package/dist/production-values.js +1 -0
- package/dist/production.d.ts +86 -0
- package/dist/production.d.ts.map +1 -0
- package/dist/production.js +1 -0
- package/dist/profile-validator.d.ts +2 -2
- package/dist/profile-validator.d.ts.map +1 -1
- package/dist/profile-validator.js +1 -783
- package/dist/recipes.js +1 -34
- package/dist/registry.js +1 -56
- package/dist/schema-profile.d.ts +23 -5
- package/dist/schema-profile.d.ts.map +1 -1
- package/dist/schema-profile.js +1 -390
- package/dist/semver.js +1 -128
- package/dist/session-policy.d.ts +23 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +1 -0
- package/dist/session.d.ts +14 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -178
- package/dist/strict-json.d.ts +7 -0
- package/dist/strict-json.d.ts.map +1 -0
- package/dist/strict-json.js +2 -0
- package/dist/url-policy.js +1 -248
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -398
- package/package.json +5 -3
- package/dist/canonical.js.map +0 -1
- package/dist/clone.js.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/contributions.js.map +0 -1
- package/dist/entry-commands.js.map +0 -1
- package/dist/extension-sdk.js.map +0 -1
- package/dist/history.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/migrations.js.map +0 -1
- package/dist/model-commands.js.map +0 -1
- package/dist/modes.js.map +0 -1
- package/dist/negotiation.js.map +0 -1
- package/dist/profile-validator.js.map +0 -1
- package/dist/recipes.js.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/schema-profile.js.map +0 -1
- package/dist/semver.js.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/url-policy.js.map +0 -1
- package/dist/validation.js.map +0 -1
package/dist/semver.js
CHANGED
|
@@ -1,128 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
export function parseSemanticVersion(value) {
|
|
3
|
-
const match = value.length <= 100 ? VERSION_PATTERN.exec(value) : null;
|
|
4
|
-
if (match === null) {
|
|
5
|
-
throw new TypeError(`${value} is not a canonical semantic version.`);
|
|
6
|
-
}
|
|
7
|
-
return {
|
|
8
|
-
major: Number(match[1]),
|
|
9
|
-
minor: Number(match[2]),
|
|
10
|
-
patch: Number(match[3]),
|
|
11
|
-
prerelease: match[4] === undefined
|
|
12
|
-
? []
|
|
13
|
-
: match[4]
|
|
14
|
-
.split('.')
|
|
15
|
-
.map((part) => (/^(0|[1-9][0-9]*)$/u.test(part) ? Number(part) : part)),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/** SemVer 2.0 precedence; build metadata never participates. */
|
|
19
|
-
export function compareSemanticVersions(left, right) {
|
|
20
|
-
const a = parseSemanticVersion(left);
|
|
21
|
-
const b = parseSemanticVersion(right);
|
|
22
|
-
for (const member of ['major', 'minor', 'patch']) {
|
|
23
|
-
if (a[member] !== b[member]) {
|
|
24
|
-
return a[member] < b[member] ? -1 : 1;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (a.prerelease.length === 0 && b.prerelease.length === 0) {
|
|
28
|
-
return 0;
|
|
29
|
-
}
|
|
30
|
-
if (a.prerelease.length === 0) {
|
|
31
|
-
return 1;
|
|
32
|
-
}
|
|
33
|
-
if (b.prerelease.length === 0) {
|
|
34
|
-
return -1;
|
|
35
|
-
}
|
|
36
|
-
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
37
|
-
for (let index = 0; index < length; index += 1) {
|
|
38
|
-
const first = a.prerelease[index];
|
|
39
|
-
const second = b.prerelease[index];
|
|
40
|
-
if (first === undefined) {
|
|
41
|
-
return -1;
|
|
42
|
-
}
|
|
43
|
-
if (second === undefined) {
|
|
44
|
-
return 1;
|
|
45
|
-
}
|
|
46
|
-
if (first === second) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (typeof first === 'number' && typeof second === 'number') {
|
|
50
|
-
return first < second ? -1 : 1;
|
|
51
|
-
}
|
|
52
|
-
if (typeof first === 'number') {
|
|
53
|
-
return -1;
|
|
54
|
-
}
|
|
55
|
-
if (typeof second === 'number') {
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
return first < second ? -1 : 1;
|
|
59
|
-
}
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Expand caret and tilde shorthands into their conventional comparator
|
|
64
|
-
* conjunctions. The supported grammar is deliberately small and
|
|
65
|
-
* deterministic: exact versions, `^`/`~` shorthands, and space-joined
|
|
66
|
-
* comparators; disjunctions are unsupported until the compatibility
|
|
67
|
-
* contract ratifies them.
|
|
68
|
-
*/
|
|
69
|
-
export function normalizeVersionRange(range) {
|
|
70
|
-
const trimmed = range.trim();
|
|
71
|
-
if (trimmed.length === 0 || trimmed.length > 120 || trimmed.includes('||')) {
|
|
72
|
-
throw new TypeError(`${range} is not a supported version range.`);
|
|
73
|
-
}
|
|
74
|
-
return trimmed
|
|
75
|
-
.split(/\s+/u)
|
|
76
|
-
.map((part) => {
|
|
77
|
-
if (part.startsWith('^')) {
|
|
78
|
-
const source = part.slice(1);
|
|
79
|
-
const version = parseSemanticVersion(source);
|
|
80
|
-
const upper = version.major > 0
|
|
81
|
-
? `${version.major + 1}.0.0-0`
|
|
82
|
-
: version.minor > 0
|
|
83
|
-
? `0.${version.minor + 1}.0-0`
|
|
84
|
-
: `0.0.${version.patch + 1}-0`;
|
|
85
|
-
return `>=${source} <${upper}`;
|
|
86
|
-
}
|
|
87
|
-
if (part.startsWith('~')) {
|
|
88
|
-
const source = part.slice(1);
|
|
89
|
-
const version = parseSemanticVersion(source);
|
|
90
|
-
return `>=${source} <${version.major}.${version.minor + 1}.0-0`;
|
|
91
|
-
}
|
|
92
|
-
parseComparator(part);
|
|
93
|
-
return part;
|
|
94
|
-
})
|
|
95
|
-
.join(' ');
|
|
96
|
-
}
|
|
97
|
-
/** Whether a version satisfies every comparator of the supported range grammar. */
|
|
98
|
-
export function satisfiesVersionRange(version, range) {
|
|
99
|
-
parseSemanticVersion(version);
|
|
100
|
-
const comparators = normalizeVersionRange(range).split(/\s+/u).map(parseComparator);
|
|
101
|
-
return comparators.every((comparator) => {
|
|
102
|
-
const comparison = compareSemanticVersions(version, comparator.version);
|
|
103
|
-
switch (comparator.operator) {
|
|
104
|
-
case '<':
|
|
105
|
-
return comparison < 0;
|
|
106
|
-
case '<=':
|
|
107
|
-
return comparison <= 0;
|
|
108
|
-
case '=':
|
|
109
|
-
return comparison === 0;
|
|
110
|
-
case '>':
|
|
111
|
-
return comparison > 0;
|
|
112
|
-
default:
|
|
113
|
-
return comparison >= 0;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
function parseComparator(part) {
|
|
118
|
-
const match = /^(>=|<=|>|<|=)?([^<>=].*)$/u.exec(part);
|
|
119
|
-
if (match?.[2] === undefined) {
|
|
120
|
-
throw new TypeError(`${part} is not a supported version comparator.`);
|
|
121
|
-
}
|
|
122
|
-
parseSemanticVersion(match[2]);
|
|
123
|
-
return {
|
|
124
|
-
operator: (match[1] ?? '='),
|
|
125
|
-
version: match[2],
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
//# sourceMappingURL=semver.js.map
|
|
1
|
+
const e=/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;export function parseSemanticVersion(t){let n=t.length<=100?e.exec(t):null;if(n===null)throw TypeError(`${t} is not a canonical semantic version.`);return{major:Number(n[1]),minor:Number(n[2]),patch:Number(n[3]),prerelease:n[4]===void 0?[]:n[4].split(`.`).map(e=>/^(0|[1-9][0-9]*)$/u.test(e)?Number(e):e)}}export function compareSemanticVersions(e,n){let r=parseSemanticVersion(e),i=parseSemanticVersion(n);for(let e of[`major`,`minor`,`patch`])if(r[e]!==i[e])return r[e]<i[e]?-1:1;if(r.prerelease.length===0&&i.prerelease.length===0)return 0;if(r.prerelease.length===0)return 1;if(i.prerelease.length===0)return-1;let a=Math.max(r.prerelease.length,i.prerelease.length);for(let e=0;e<a;e+=1){let t=r.prerelease[e],n=i.prerelease[e];if(t===void 0)return-1;if(n===void 0)return 1;if(t!==n)return typeof t==`number`&&typeof n==`number`?t<n?-1:1:typeof t==`number`?-1:typeof n==`number`?1:t<n?-1:1}return 0}export function normalizeVersionRange(e){let n=e.trim();if(n.length===0||n.length>120||n.includes(`||`))throw TypeError(`${e} is not a supported version range.`);return n.split(/\s+/u).map(e=>{if(e.startsWith(`^`)){let n=e.slice(1),r=parseSemanticVersion(n);return`>=${n} <${r.major>0?`${r.major+1}.0.0-0`:r.minor>0?`0.${r.minor+1}.0-0`:`0.0.${r.patch+1}-0`}`}if(e.startsWith(`~`)){let n=e.slice(1),r=parseSemanticVersion(n);return`>=${n} <${r.major}.${r.minor+1}.0-0`}return a(e),e}).join(` `)}export function satisfiesVersionRange(e,i){return parseSemanticVersion(e),normalizeVersionRange(i).split(/\s+/u).map(a).every(t=>{let r=compareSemanticVersions(e,t.version);switch(t.operator){case`<`:return r<0;case`<=`:return r<=0;case`=`:return r===0;case`>`:return r>0;default:return r>=0}})}function a(e){let n=/^(>=|<=|>|<|=)?([^<>=].*)$/u.exec(e);if(n?.[2]===void 0)throw TypeError(`${e} is not a supported version comparator.`);return parseSemanticVersion(n[2]),{operator:n[1]??`=`,version:n[2]}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BlueprintCommand, BlueprintDocument, ContentModelDocument, EntryDocument, QualifiedName, StudioLimits } from '@kumwe/studio-protocol';
|
|
2
|
+
export type StudioCommandPolicyLimits = Pick<StudioLimits, 'maxChildrenPerSlot' | 'maxCommandBatch' | 'maxDepth' | 'maxExtensionBytes' | 'maxNodes' | 'maxPropertyBytes' | 'maxRichTextBytes' | 'maxRichTextDepth' | 'maxSlotsPerNode'>;
|
|
3
|
+
export interface StudioSessionPolicyOptions {
|
|
4
|
+
limits?: Readonly<Partial<StudioCommandPolicyLimits>>;
|
|
5
|
+
permissions?: readonly QualifiedName[];
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedStudioSessionPolicy {
|
|
8
|
+
limits: Readonly<StudioCommandPolicyLimits>;
|
|
9
|
+
permissions: ReadonlySet<QualifiedName>;
|
|
10
|
+
}
|
|
11
|
+
/** Immutable protocol ceilings used when a low-level session has no host configuration. */
|
|
12
|
+
export declare const STUDIO_PROTOCOL_MAXIMUM_COMMAND_POLICY_LIMITS: Readonly<StudioCommandPolicyLimits>;
|
|
13
|
+
export declare function resolveStudioSessionPolicy(options?: Readonly<StudioSessionPolicyOptions>): ResolvedStudioSessionPolicy;
|
|
14
|
+
/**
|
|
15
|
+
* Checks the complete projected Blueprint before history commits it. The
|
|
16
|
+
* reducer has already accepted the command, so this function may inspect both
|
|
17
|
+
* sides of a valid transition without changing either one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertBlueprintCommandPolicy(before: BlueprintDocument, command: BlueprintCommand, after: BlueprintDocument, policy: Readonly<ResolvedStudioSessionPolicy>): void;
|
|
20
|
+
export declare function assertBlueprintWithinSessionPolicy(document: BlueprintDocument, limits: Readonly<StudioCommandPolicyLimits>): void;
|
|
21
|
+
export declare function assertEntryWithinSessionPolicy(entry: EntryDocument, limits: Readonly<StudioCommandPolicyLimits>): void;
|
|
22
|
+
export declare function assertModelWithinSessionPolicy(model: ContentModelDocument, limits: Readonly<StudioCommandPolicyLimits>): void;
|
|
23
|
+
//# sourceMappingURL=session-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-policy.d.ts","sourceRoot":"","sources":["../src/session-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,gBAAgB,EAChB,iBAAiB,EAEjB,oBAAoB,EACpB,aAAa,EAGb,aAAa,EACb,YAAY,EACb,MAAM,wBAAwB,CAAC;AAIhC,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,YAAY,EACV,oBAAoB,GACpB,iBAAiB,GACjB,UAAU,GACV,mBAAmB,GACnB,UAAU,GACV,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,CACpB,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC;IACtD,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,QAAQ,CAAC,yBAAyB,CAAC,CAAC;IAC5C,WAAW,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;CACzC;AAED,2FAA2F;AAC3F,eAAO,MAAM,6CAA6C,EAAE,QAAQ,CAAC,yBAAyB,CAW1F,CAAC;AA6BL,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,QAAQ,CAAC,0BAA0B,CAAM,GACjD,2BAA2B,CAiB7B;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,QAAQ,CAAC,2BAA2B,CAAC,GAC5C,IAAI,CAaN;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAC1C,IAAI,CAuCN;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAC1C,IAAI,CAMN;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,oBAAoB,EAC3B,MAAM,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAC1C,IAAI,CAgBN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloneContractValue as e}from"./clone.js";import{applyOperation as t,StudioCommandError as n}from"./commands.js";export const STUDIO_PROTOCOL_MAXIMUM_COMMAND_POLICY_LIMITS=Object.freeze({maxChildrenPerSlot:1e4,maxCommandBatch:1e4,maxDepth:128,maxExtensionBytes:10485760,maxNodes:1e5,maxPropertyBytes:10485760,maxRichTextBytes:10485760,maxRichTextDepth:128,maxSlotsPerNode:100});const i=Object.freeze({maxChildrenPerSlot:0,maxCommandBatch:1,maxDepth:1,maxExtensionBytes:0,maxNodes:1,maxPropertyBytes:0,maxRichTextBytes:1,maxRichTextDepth:1,maxSlotsPerNode:0});export function resolveStudioSessionPolicy(e={}){let t=e.limits??{},n={maxChildrenPerSlot:s(`maxChildrenPerSlot`,t.maxChildrenPerSlot),maxCommandBatch:s(`maxCommandBatch`,t.maxCommandBatch),maxDepth:s(`maxDepth`,t.maxDepth),maxExtensionBytes:s(`maxExtensionBytes`,t.maxExtensionBytes),maxNodes:s(`maxNodes`,t.maxNodes),maxPropertyBytes:s(`maxPropertyBytes`,t.maxPropertyBytes),maxRichTextBytes:s(`maxRichTextBytes`,t.maxRichTextBytes),maxRichTextDepth:s(`maxRichTextDepth`,t.maxRichTextDepth),maxSlotsPerNode:s(`maxSlotsPerNode`,t.maxSlotsPerNode)};return Object.freeze({limits:Object.freeze(n),permissions:new Set(e.permissions??[])})}export function assertBlueprintCommandPolicy(e,t,n,r){if(t.type===`studio.command/batch`&&t.payload.operations.length>r.limits.maxCommandBatch)throw C(`maxCommandBatch`,t.payload.operations.length,r.limits.maxCommandBatch);c(e,t,r.permissions),assertBlueprintWithinSessionPolicy(n,r.limits)}export function assertBlueprintWithinSessionPolicy(e,t){let n=v(e.extensions);if(n>t.maxExtensionBytes)throw C(`maxExtensionBytes`,n,t.maxExtensionBytes);let r=0,i=0,a=e.roots.map(e=>({depth:1,node:e}));for(;a.length>0;){let e=a.pop();if(e===void 0)break;if(r+=1,r>t.maxNodes)throw C(`maxNodes`,r,t.maxNodes);if(e.depth>t.maxDepth)throw C(`maxDepth`,e.depth,t.maxDepth);let o=Object.entries(e.node.slots);if(o.length>t.maxSlotsPerNode)throw C(`maxSlotsPerNode`,o.length,t.maxSlotsPerNode);if(i+=y(e.node.properties),i+=y(e.node.responsive),i>t.maxPropertyBytes)throw C(`maxPropertyBytes`,i,t.maxPropertyBytes);if(n+=v(e.node.extensions),n>t.maxExtensionBytes)throw C(`maxExtensionBytes`,n,t.maxExtensionBytes);for(let[,n]of o){if(n.length>t.maxChildrenPerSlot)throw C(`maxChildrenPerSlot`,n.length,t.maxChildrenPerSlot);for(let t of n)a.push({depth:e.depth+1,node:t})}}g(e,t)}export function assertEntryWithinSessionPolicy(e,t){let n=v(e.extensions);if(n>t.maxExtensionBytes)throw C(`maxExtensionBytes`,n,t.maxExtensionBytes);g(e,t)}export function assertModelWithinSessionPolicy(e,t){let n=v(e.extensions);for(let t of e.relationships)n+=v(t.extensions);let r=[...e.fields];for(;r.length>0;){let e=r.pop();if(e===void 0)break;n+=v(e.extensions),r.push(...e.fields??[])}if(n>t.maxExtensionBytes)throw C(`maxExtensionBytes`,n,t.maxExtensionBytes);g(e,t)}function s(e,t){let n=STUDIO_PROTOCOL_MAXIMUM_COMMAND_POLICY_LIMITS[e],a=t??n;if(!Number.isSafeInteger(a)||a<i[e]||a>n)throw RangeError(`${e} must be an integer between ${String(i[e])} and ${String(n)}.`);return a}function c(n,r,i){if(r.type===`studio.command/batch`){let a=e(n);for(let e of r.payload.operations)l(a,e,i),t(a,e);return}if(r.type===`studio.command/apply-pattern`){u(n,r.payload.destination.parentNodeId,i);for(let e of r.payload.nodes)f(e,i);return}if(r.type===`studio.command/reset-inherited-property`){d(n,r.payload.nodeId,i);return}l(n,r,i)}function l(e,t,n){switch(t.type){case`studio.command/insert-node`:case`studio.command/restore-node`:u(e,t.payload.destination.parentNodeId,n),f(t.payload.node,n);return;case`studio.command/remove-node`:{let r=m(e,t.payload.nodeId);p(r.parent,n),f(r.node,n);return}case`studio.command/move-node`:{let r=m(e,t.payload.nodeId);p(r.parent,n),f(r.node,n),u(e,t.payload.destination.parentNodeId,n);return}case`studio.command/duplicate-node`:{let r=m(e,t.payload.nodeId);p(r.parent,n),f(r.node,n),u(e,t.payload.destination?.parentNodeId,n);return}case`studio.command/reorder-children`:u(e,t.payload.parentNodeId,n);for(let r of t.payload.order)d(e,r,n);return;case`studio.command/remove-binding`:case`studio.command/set-binding`:case`studio.command/set-property`:case`studio.command/set-size-role`:case`studio.command/unset-property`:case`studio.command/unset-size-role`:d(e,t.payload.nodeId,n);return}}function u(e,t,n){t!==void 0&&d(e,t,n)}function d(e,t,n){p(m(e,t).node,n)}function f(e,t){let n=[e];for(;n.length>0;){let e=n.pop();if(e===void 0)break;p(e,t);for(let t of Object.values(e.slots))n.push(...t)}}function p(e,t){let r=e?.authoring.requiredPermission;if(r!==void 0&&!t.has(r))throw new n(`permission-forbidden`,`Node ${String(e?.id)} requires the ${r} permission for this command.`)}function m(e,t){let r=h(e.roots,t);if(r===void 0)throw new n(`node-not-found`,`Node ${t} does not exist.`);return r}function h(e,t,n){for(let r of e){if(r.id===t)return n===void 0?{node:r}:{node:r,parent:n};for(let e of Object.values(r.slots)){let n=h(e,t,r);if(n!==void 0)return n}}}function g(e,t){let n=[e];for(;n.length>0;){let e=n.pop();if(!(typeof e!=`object`||!e)){if(Array.isArray(e)){n.push(...e);continue}if(e.type===`doc`&&Array.isArray(e.content)){let n=b(e);if(n>t.maxRichTextBytes)throw C(`maxRichTextBytes`,n,t.maxRichTextBytes);let r=_(e);if(r>t.maxRichTextDepth)throw C(`maxRichTextDepth`,r,t.maxRichTextDepth);continue}for(let t of Object.values(e))n.push(t)}}}function _(e){let t=1,n=[{depth:1,node:e}];for(;n.length>0;){let e=n.pop();if(e===void 0)break;t=Math.max(t,e.depth);let r=e.node.content;if(Array.isArray(r))for(let t of r)S(t)&&n.push({depth:e.depth+1,node:t})}return t}function v(e){return y(e)}function y(e){return e===void 0||Object.keys(e).length===0?0:Math.max(0,b(e)-2)}function b(e){let t;try{let n=JSON.stringify(e);if(n===void 0)throw TypeError(`The value is not JSON serializable.`);t=n}catch{throw new n(`resource-limit`,`The command value cannot be measured within the finite JSON resource boundary.`)}return x(t)}function x(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r<=127)t+=1;else if(r<=2047)t+=2;else if(r>=55296&&r<=56319){let r=e.charCodeAt(n+1);r>=56320&&r<=57343?(t+=4,n+=1):t+=3}else t+=3}return t}function S(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function C(e,t,r){return new n(`resource-limit`,`${e} permits at most ${String(r)}, but the projected command requires ${String(t)}.`)}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { AddModelFieldCommand, BlueprintCommand, BlueprintDocument, ContentModelDocument, EntryDocument, NodeId, Revision, SetFieldValueCommand, StudioSessionMode } from '@kumwe/studio-protocol';
|
|
1
|
+
import type { AddModelFieldCommand, BlueprintCommand, BlueprintDocument, ContentModelDocument, EntryDocument, NodeId, QualifiedName, Revision, SetFieldValueCommand, StudioSessionMode } from '@kumwe/studio-protocol';
|
|
2
|
+
import { type StudioCommandPolicyLimits } from './session-policy.js';
|
|
2
3
|
export interface StudioSessionOptions {
|
|
3
4
|
document: BlueprintDocument;
|
|
5
|
+
/** Command-time resource policy. Omitted members use immutable protocol maxima. */
|
|
6
|
+
limits?: Readonly<Partial<StudioCommandPolicyLimits>>;
|
|
4
7
|
maximumHistoryEntries?: number;
|
|
5
8
|
/**
|
|
6
9
|
* The session mode fixed for the session's lifetime. When omitted it is
|
|
@@ -9,6 +12,8 @@ export interface StudioSessionOptions {
|
|
|
9
12
|
* mode the session historically provided.
|
|
10
13
|
*/
|
|
11
14
|
mode?: StudioSessionMode;
|
|
15
|
+
/** Exact resolved permission projection used by per-node authoring policy. */
|
|
16
|
+
permissions?: readonly QualifiedName[];
|
|
12
17
|
sessionGeneration: Revision;
|
|
13
18
|
/**
|
|
14
19
|
* The legacy spelling of the read-only axis. Mode `read-only` is the
|
|
@@ -56,7 +61,14 @@ export declare class StudioSession {
|
|
|
56
61
|
* Blueprint history, and the selection is untouched.
|
|
57
62
|
*/
|
|
58
63
|
executeModelCommand(model: ContentModelDocument, command: AddModelFieldCommand): ContentModelDocument;
|
|
59
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Records the host revision that accepted a local snapshot and rebases every
|
|
66
|
+
* undo/redo snapshot onto it. Callers normally omit `stateVersion`, which
|
|
67
|
+
* marks the current document clean. A host save that settles after another
|
|
68
|
+
* local edit passes the captured snapshot version instead: the accepted base
|
|
69
|
+
* revision advances while the newer draft stays dirty.
|
|
70
|
+
*/
|
|
71
|
+
markSaved(revision: Revision, stateVersion?: number): void;
|
|
60
72
|
get savedRevision(): Revision;
|
|
61
73
|
select(nodeIds: readonly NodeId[]): readonly NodeId[];
|
|
62
74
|
clearSelection(): void;
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EAEjB,oBAAoB,EACpB,aAAa,EACb,MAAM,EACN,QAAQ,EACR,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EAEjB,oBAAoB,EACpB,aAAa,EACb,MAAM,EACN,aAAa,EACb,QAAQ,EACR,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EAKL,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,mFAAmF;IACnF,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACvC,iBAAiB,EAAE,QAAQ,CAAC;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;CACzC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,aAAa;;gBASL,OAAO,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IAe1D,IAAW,OAAO,IAAI,OAAO,CAE5B;IAED,IAAW,OAAO,IAAI,OAAO,CAE5B;IAED,IAAW,KAAK,IAAI,OAAO,CAE1B;IAED,IAAW,QAAQ,IAAI,iBAAiB,CAEvC;IAED,0CAA0C;IAC1C,IAAW,IAAI,IAAI,iBAAiB,CAEnC;IAED,IAAW,SAAS,IAAI,SAAS,MAAM,EAAE,CAExC;IAED,2DAA2D;IAC3D,IAAW,YAAY,IAAI,UAAU,GAAG,WAAW,CAElD;IAED,IAAW,YAAY,IAAI,MAAM,CAEhC;IAEM,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,iBAAiB;IAqB5D;;;;;OAKG;IACI,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,oBAAoB,GAAG,aAAa;IAe9F;;;;;OAKG;IACI,mBAAmB,CACxB,KAAK,EAAE,oBAAoB,EAC3B,OAAO,EAAE,oBAAoB,GAC5B,oBAAoB;IAevB;;;;;;OAMG;IACI,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,GAAE,MAAmC,GAAG,IAAI;IAa7F,IAAW,aAAa,IAAI,QAAQ,CAEnC;IAEM,MAAM,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE;IAmBrD,cAAc,IAAI,IAAI;IAItB,IAAI,IAAI,iBAAiB;IAMzB,IAAI,IAAI,iBAAiB;CA6BjC"}
|
package/dist/session.js
CHANGED
|
@@ -1,178 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { applyEntryCommand } from './entry-commands.js';
|
|
3
|
-
import { StudioHistory } from './history.js';
|
|
4
|
-
import { applyModelCommand } from './model-commands.js';
|
|
5
|
-
import { assertHybridCommandInBounds, assertModePermitsCommandType } from './modes.js';
|
|
6
|
-
/**
|
|
7
|
-
* A deterministic editing session over one Blueprint document: bounded
|
|
8
|
-
* history, an explicit selection model, and the session-level guards the
|
|
9
|
-
* command contract requires before any reducer runs. Session generation,
|
|
10
|
-
* mode permission, read-only state, and expected-revision checks fail
|
|
11
|
-
* closed; a rejected command changes neither the document, the history, nor
|
|
12
|
-
* the selection. The session mode is fixed at creation and decides the
|
|
13
|
-
* permitted command set through one deterministic table
|
|
14
|
-
* (`permittedCommandTypes`); hybrid mode additionally bounds structure
|
|
15
|
-
* commands to slots governed by structural authoring regions.
|
|
16
|
-
*/
|
|
17
|
-
export class StudioSession {
|
|
18
|
-
#history;
|
|
19
|
-
#mode;
|
|
20
|
-
#sessionGeneration;
|
|
21
|
-
#savedRevision;
|
|
22
|
-
#savedStateVersion = 0;
|
|
23
|
-
#selection = [];
|
|
24
|
-
constructor(options) {
|
|
25
|
-
this.#history = new StudioHistory(options.document, options.maximumHistoryEntries ?? 100);
|
|
26
|
-
this.#mode = resolveModeOption(options);
|
|
27
|
-
this.#sessionGeneration = options.sessionGeneration;
|
|
28
|
-
this.#savedRevision = options.document.revision;
|
|
29
|
-
}
|
|
30
|
-
get canRedo() {
|
|
31
|
-
return this.#history.canRedo;
|
|
32
|
-
}
|
|
33
|
-
get canUndo() {
|
|
34
|
-
return this.#history.canUndo;
|
|
35
|
-
}
|
|
36
|
-
get dirty() {
|
|
37
|
-
return this.#history.stateVersion !== this.#savedStateVersion;
|
|
38
|
-
}
|
|
39
|
-
get document() {
|
|
40
|
-
return this.#history.current;
|
|
41
|
-
}
|
|
42
|
-
/** The session mode fixed at creation. */
|
|
43
|
-
get mode() {
|
|
44
|
-
return this.#mode;
|
|
45
|
-
}
|
|
46
|
-
get selection() {
|
|
47
|
-
return [...this.#selection];
|
|
48
|
-
}
|
|
49
|
-
/** The legacy read-only projection of the session mode. */
|
|
50
|
-
get sessionState() {
|
|
51
|
-
return this.#mode === 'read-only' ? 'read-only' : 'editable';
|
|
52
|
-
}
|
|
53
|
-
get stateVersion() {
|
|
54
|
-
return this.#history.stateVersion;
|
|
55
|
-
}
|
|
56
|
-
execute(command) {
|
|
57
|
-
this.#assertWritable();
|
|
58
|
-
this.#assertLiveGeneration(command);
|
|
59
|
-
assertModePermitsCommandType(this.#mode, command.type);
|
|
60
|
-
if (this.#mode === 'hybrid') {
|
|
61
|
-
assertHybridCommandInBounds(this.#history.current, command);
|
|
62
|
-
}
|
|
63
|
-
if (command.expectedRevision !== undefined &&
|
|
64
|
-
command.expectedRevision !== this.#savedRevision) {
|
|
65
|
-
throw new StudioCommandError('stale-state', `Command expects revision ${command.expectedRevision}, but the session holds ${this.#savedRevision}.`);
|
|
66
|
-
}
|
|
67
|
-
const next = this.#history.execute(command);
|
|
68
|
-
this.#pruneSelection(next);
|
|
69
|
-
return next;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Applies one entry field command behind the same session guards as
|
|
73
|
-
* `execute`, so no UI dispatch path bypasses the mode boundary. Entry
|
|
74
|
-
* state stays host-owned: the result is returned, not recorded in the
|
|
75
|
-
* Blueprint history, and the selection is untouched.
|
|
76
|
-
*/
|
|
77
|
-
executeEntryCommand(entry, command) {
|
|
78
|
-
this.#assertWritable();
|
|
79
|
-
this.#assertLiveGeneration(command);
|
|
80
|
-
assertModePermitsCommandType(this.#mode, command.type);
|
|
81
|
-
if (command.expectedRevision !== undefined && command.expectedRevision !== entry.revision) {
|
|
82
|
-
throw new StudioCommandError('stale-state', `Command expects revision ${command.expectedRevision}, but the entry holds ${entry.revision}.`);
|
|
83
|
-
}
|
|
84
|
-
return applyEntryCommand(entry, command);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Applies one content-model command behind the same session guards as
|
|
88
|
-
* `execute`, so no UI dispatch path bypasses the mode boundary. Model
|
|
89
|
-
* state stays host-owned: the result is returned, not recorded in the
|
|
90
|
-
* Blueprint history, and the selection is untouched.
|
|
91
|
-
*/
|
|
92
|
-
executeModelCommand(model, command) {
|
|
93
|
-
this.#assertWritable();
|
|
94
|
-
this.#assertLiveGeneration(command);
|
|
95
|
-
assertModePermitsCommandType(this.#mode, command.type);
|
|
96
|
-
if (command.expectedRevision !== undefined && command.expectedRevision !== model.revision) {
|
|
97
|
-
throw new StudioCommandError('stale-state', `Command expects revision ${command.expectedRevision}, but the model holds ${model.revision}.`);
|
|
98
|
-
}
|
|
99
|
-
return applyModelCommand(model, command);
|
|
100
|
-
}
|
|
101
|
-
markSaved(revision) {
|
|
102
|
-
this.#savedRevision = revision;
|
|
103
|
-
this.#savedStateVersion = this.#history.stateVersion;
|
|
104
|
-
}
|
|
105
|
-
get savedRevision() {
|
|
106
|
-
return this.#savedRevision;
|
|
107
|
-
}
|
|
108
|
-
select(nodeIds) {
|
|
109
|
-
const document = this.#history.current;
|
|
110
|
-
const unique = [];
|
|
111
|
-
for (const nodeId of nodeIds) {
|
|
112
|
-
if (unique.includes(nodeId)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!containsNode(document.roots, nodeId)) {
|
|
116
|
-
throw new StudioCommandError('node-not-found', `Node ${nodeId} cannot be selected because it is not in the document.`);
|
|
117
|
-
}
|
|
118
|
-
unique.push(nodeId);
|
|
119
|
-
}
|
|
120
|
-
this.#selection = unique;
|
|
121
|
-
return this.selection;
|
|
122
|
-
}
|
|
123
|
-
clearSelection() {
|
|
124
|
-
this.#selection = [];
|
|
125
|
-
}
|
|
126
|
-
undo() {
|
|
127
|
-
const document = this.#history.undo();
|
|
128
|
-
this.#pruneSelection(document);
|
|
129
|
-
return document;
|
|
130
|
-
}
|
|
131
|
-
redo() {
|
|
132
|
-
const document = this.#history.redo();
|
|
133
|
-
this.#pruneSelection(document);
|
|
134
|
-
return document;
|
|
135
|
-
}
|
|
136
|
-
#assertWritable() {
|
|
137
|
-
if (this.#mode === 'read-only') {
|
|
138
|
-
throw new StudioCommandError('read-only-session', 'A read-only session never applies a persistent command.');
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
#assertLiveGeneration(command) {
|
|
142
|
-
if (command.sessionGeneration !== this.#sessionGeneration) {
|
|
143
|
-
throw new StudioCommandError('stale-generation', `Command generation ${command.sessionGeneration} does not match the active session generation.`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
#pruneSelection(document) {
|
|
147
|
-
if (this.#selection.length > 0) {
|
|
148
|
-
this.#selection = this.#selection.filter((nodeId) => containsNode(document.roots, nodeId));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
function resolveModeOption(options) {
|
|
153
|
-
const { mode, sessionState } = options;
|
|
154
|
-
if (mode === undefined) {
|
|
155
|
-
if (sessionState === undefined) {
|
|
156
|
-
throw new RangeError('A session requires an explicit mode or session state.');
|
|
157
|
-
}
|
|
158
|
-
return sessionState === 'read-only' ? 'read-only' : 'blueprint';
|
|
159
|
-
}
|
|
160
|
-
if (sessionState !== undefined && (sessionState === 'read-only') !== (mode === 'read-only')) {
|
|
161
|
-
throw new RangeError(`Session mode ${mode} contradicts session state ${sessionState}; mode read-only is the read-only state.`);
|
|
162
|
-
}
|
|
163
|
-
return mode;
|
|
164
|
-
}
|
|
165
|
-
function containsNode(nodes, nodeId) {
|
|
166
|
-
for (const node of nodes) {
|
|
167
|
-
if (node.id === nodeId) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
for (const children of Object.values(node.slots)) {
|
|
171
|
-
if (containsNode(children, nodeId)) {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
//# sourceMappingURL=session.js.map
|
|
1
|
+
import{StudioCommandError as e}from"./commands.js";import{applyEntryCommand as t}from"./entry-commands.js";import{StudioHistory as n}from"./history.js";import{applyModelCommand as r}from"./model-commands.js";import{assertHybridCommandInBounds as i,assertModePermitsCommandType as a}from"./modes.js";import{assertEntryWithinSessionPolicy as o,assertModelWithinSessionPolicy as s,resolveStudioSessionPolicy as c}from"./session-policy.js";export class StudioSession{#history;#mode;#policy;#sessionGeneration;#savedRevision;#savedStateVersion=0;#selection=[];constructor(e){this.#policy=c({...e.limits===void 0?{}:{limits:e.limits},...e.permissions===void 0?{}:{permissions:e.permissions}}),this.#history=new n(e.document,e.maximumHistoryEntries??100,this.#policy),this.#mode=l(e),this.#sessionGeneration=e.sessionGeneration,this.#savedRevision=e.document.revision}get canRedo(){return this.#history.canRedo}get canUndo(){return this.#history.canUndo}get dirty(){return this.#history.stateVersion!==this.#savedStateVersion}get document(){return this.#history.current}get mode(){return this.#mode}get selection(){return[...this.#selection]}get sessionState(){return this.#mode===`read-only`?`read-only`:`editable`}get stateVersion(){return this.#history.stateVersion}execute(t){if(this.#assertWritable(),this.#assertLiveGeneration(t),a(this.#mode,t.type),this.#mode===`hybrid`&&i(this.#history.current,t),t.expectedRevision!==void 0&&t.expectedRevision!==this.#savedRevision)throw new e(`stale-state`,`Command expects revision ${t.expectedRevision}, but the session holds ${this.#savedRevision}.`);let n=this.#history.execute(t);return this.#pruneSelection(n),n}executeEntryCommand(n,r){if(this.#assertWritable(),this.#assertLiveGeneration(r),a(this.#mode,r.type),r.expectedRevision!==void 0&&r.expectedRevision!==n.revision)throw new e(`stale-state`,`Command expects revision ${r.expectedRevision}, but the entry holds ${n.revision}.`);let i=t(n,r);return o(i,this.#policy.limits),i}executeModelCommand(t,n){if(this.#assertWritable(),this.#assertLiveGeneration(n),a(this.#mode,n.type),n.expectedRevision!==void 0&&n.expectedRevision!==t.revision)throw new e(`stale-state`,`Command expects revision ${n.expectedRevision}, but the model holds ${t.revision}.`);let i=r(t,n);return s(i,this.#policy.limits),i}markSaved(e,t=this.#history.stateVersion){if(!Number.isSafeInteger(t)||t<0||t>this.#history.stateVersion)throw RangeError(`A saved snapshot version must be a known non-negative state version.`);this.#history.rebaseRevision(e),this.#savedRevision=e,this.#savedStateVersion=t}get savedRevision(){return this.#savedRevision}select(t){let n=this.#history.current,r=[];for(let i of t)if(!r.includes(i)){if(!u(n.roots,i))throw new e(`node-not-found`,`Node ${i} cannot be selected because it is not in the document.`);r.push(i)}return this.#selection=r,this.selection}clearSelection(){this.#selection=[]}undo(){let e=this.#history.undo();return this.#pruneSelection(e),e}redo(){let e=this.#history.redo();return this.#pruneSelection(e),e}#assertWritable(){if(this.#mode===`read-only`)throw new e(`read-only-session`,`A read-only session never applies a persistent command.`)}#assertLiveGeneration(t){if(t.sessionGeneration!==this.#sessionGeneration)throw new e(`stale-generation`,`Command generation ${t.sessionGeneration} does not match the active session generation.`)}#pruneSelection(e){this.#selection.length>0&&(this.#selection=this.#selection.filter(t=>u(e.roots,t)))}}function l(e){let{mode:t,sessionState:n}=e;if(t===void 0){if(n===void 0)throw RangeError(`A session requires an explicit mode or session state.`);return n===`read-only`?`read-only`:`blueprint`}if(n!==void 0&&n===`read-only`!=(t===`read-only`))throw RangeError(`Session mode ${t} contradicts session state ${n}; mode read-only is the read-only state.`);return t}function u(e,t){for(let n of e){if(n.id===t)return!0;for(let e of Object.values(n.slots))if(u(e,t))return!0}return!1}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse untrusted JSON while rejecting duplicate object member names.
|
|
3
|
+
* Native `JSON.parse` silently keeps the final duplicate; configuration must
|
|
4
|
+
* fail closed instead so routing/authentication cannot be visually shadowed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseJsonRejectingDuplicateMembers(source: string, maximumDepth?: number): unknown;
|
|
7
|
+
//# sourceMappingURL=strict-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strict-json.d.ts","sourceRoot":"","sources":["../src/strict-json.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,SAAK,GAAG,OAAO,CAI7F"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export function parseJsonRejectingDuplicateMembers(e,n=16){return new t(e,n).assertValid(),JSON.parse(e)}class t{#cursor=0;#maximumDepth;#numberPattern=/-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;#source;constructor(e,t){if(!Number.isSafeInteger(t)||t<1)throw RangeError(`Maximum JSON depth must be a positive safe integer.`);this.#source=e,this.#maximumDepth=t}assertValid(){this.#parseValue(``,1),this.#skipWhitespace(),this.#cursor!==this.#source.length&&this.#fail(`unexpected trailing content`)}#escapePointer(e){return e.replaceAll(`~`,`~0`).replaceAll(`/`,`~1`)}#expect(e){this.#source[this.#cursor]!==e&&this.#fail(`expected ${JSON.stringify(e)}`),this.#cursor+=1}#fail(e){throw SyntaxError(`Invalid JSON: ${e} at offset ${this.#cursor}.`)}#parseArray(e,t){if(this.#expect(`[`),this.#skipWhitespace(),this.#source[this.#cursor]===`]`){this.#cursor+=1;return}let n=0;for(;;){if(this.#parseValue(`${e}/${n}`,t+1),n+=1,this.#skipWhitespace(),this.#source[this.#cursor]===`]`){this.#cursor+=1;return}this.#expect(`,`),this.#skipWhitespace()}}#parseLiteral(e){this.#source.slice(this.#cursor,this.#cursor+e.length)!==e&&this.#fail(`invalid token`),this.#cursor+=e.length}#parseNumber(){this.#numberPattern.lastIndex=this.#cursor,this.#numberPattern.exec(this.#source)?.index!==this.#cursor&&this.#fail(`invalid number`),this.#cursor=this.#numberPattern.lastIndex}#parseObject(e,t){if(this.#expect(`{`),this.#skipWhitespace(),this.#source[this.#cursor]===`}`){this.#cursor+=1;return}let n=new Set;for(;;){this.#source[this.#cursor]!==`"`&&this.#fail(`object member name must be a string`);let r=this.#parseString();if(n.has(r))throw SyntaxError(`Invalid JSON: duplicate member ${JSON.stringify(r)} at ${e||`/`}.`);if(n.add(r),this.#skipWhitespace(),this.#expect(`:`),this.#parseValue(`${e}/${this.#escapePointer(r)}`,t+1),this.#skipWhitespace(),this.#source[this.#cursor]===`}`){this.#cursor+=1;return}this.#expect(`,`),this.#skipWhitespace()}}#parseString(){let e=this.#cursor;for(this.#expect(`"`);this.#cursor<this.#source.length;){let t=this.#source[this.#cursor];if(t===`"`)return this.#cursor+=1,JSON.parse(this.#source.slice(e,this.#cursor));if(t===`\\`){this.#cursor+=1;let e=this.#source[this.#cursor];if(e===`u`){/^[0-9A-Fa-f]{4}$/u.test(this.#source.slice(this.#cursor+1,this.#cursor+5))||this.#fail(`invalid Unicode escape`),this.#cursor+=5;continue}`"\\/bfnrt`.includes(e??``)||this.#fail(`invalid string escape`),this.#cursor+=1;continue}(t===void 0||t.charCodeAt(0)<32)&&this.#fail(`invalid string character`),this.#cursor+=1}this.#fail(`unterminated string`)}#parseValue(e,t){switch(this.#skipWhitespace(),this.#source[this.#cursor]){case`{`:this.#assertContainerDepth(t),this.#parseObject(e,t);return;case`[`:this.#assertContainerDepth(t),this.#parseArray(e,t);return;case`"`:this.#parseString();return;case`t`:this.#parseLiteral(`true`);return;case`f`:this.#parseLiteral(`false`);return;case`n`:this.#parseLiteral(`null`);return;default:this.#parseNumber()}}#assertContainerDepth(e){if(e>this.#maximumDepth)throw SyntaxError(`Invalid JSON: document exceeds maximum depth ${String(this.#maximumDepth)}.`)}#skipWhitespace(){for(;;){let e=this.#source[this.#cursor];if(e!==` `&&e!==` `&&e!==`
|
|
2
|
+
`&&e!==`\r`)return;this.#cursor+=1}}}
|