@microsoft/managed-apps-dev-gateway 0.3.1
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/README.md +187 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/DevelopmentMiddleware.d.ts +20 -0
- package/dist/resources/DevelopmentMiddleware.d.ts.map +1 -0
- package/dist/resources/DevelopmentMiddleware.js +117 -0
- package/dist/resources/DevelopmentMiddleware.js.map +1 -0
- package/dist/resources/PackagedResources.d.ts +10 -0
- package/dist/resources/PackagedResources.d.ts.map +1 -0
- package/dist/resources/PackagedResources.js +14 -0
- package/dist/resources/PackagedResources.js.map +1 -0
- package/dist/resources/appScript.js +69 -0
- package/dist/resources/loader.html +98 -0
- package/dist/resources/localDevWorker.js +68 -0
- package/dist/snapshot/OobRevision.d.ts +38 -0
- package/dist/snapshot/OobRevision.d.ts.map +1 -0
- package/dist/snapshot/OobRevision.js +40 -0
- package/dist/snapshot/OobRevision.js.map +1 -0
- package/dist/snapshot/PathPolicy.d.ts +69 -0
- package/dist/snapshot/PathPolicy.d.ts.map +1 -0
- package/dist/snapshot/PathPolicy.js +129 -0
- package/dist/snapshot/PathPolicy.js.map +1 -0
- package/dist/snapshot/SnapshotReader.d.ts +63 -0
- package/dist/snapshot/SnapshotReader.d.ts.map +1 -0
- package/dist/snapshot/SnapshotReader.js +182 -0
- package/dist/snapshot/SnapshotReader.js.map +1 -0
- package/dist/snapshot/SnapshotSource.d.ts +49 -0
- package/dist/snapshot/SnapshotSource.d.ts.map +1 -0
- package/dist/snapshot/SnapshotSource.js +81 -0
- package/dist/snapshot/SnapshotSource.js.map +1 -0
- package/dist/snapshot/index.d.ts +9 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +8 -0
- package/dist/snapshot/index.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic JSON with object keys sorted at every depth. Array order is
|
|
6
|
+
* preserved because it is meaningful in JSON Schema. Used only to derive the
|
|
7
|
+
* effective OOB revision, never to produce the bytes sent to the backend.
|
|
8
|
+
*/
|
|
9
|
+
export declare function canonicalJson(value: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* Opaque revision for the effective OOB content. It covers exactly what the
|
|
12
|
+
* inherited `validateDevelopmentSnapshotUpdate` treats as OOB-affecting — app
|
|
13
|
+
* identity, the inline manifest, and the effective environment identity — so it
|
|
14
|
+
* changes if and only if one of those changes. Formatting-only, asset-only and
|
|
15
|
+
* connector-only edits leave it unchanged and keep the backend session valid.
|
|
16
|
+
* Key order is insensitive because the digest is taken over canonical JSON.
|
|
17
|
+
* The digest is a local change detector, not a security or integrity claim.
|
|
18
|
+
*
|
|
19
|
+
* Identity is normalized the same way the inherited comparison normalizes it:
|
|
20
|
+
* app IDs are compared case-insensitively, so they are lowercased here, while
|
|
21
|
+
* the environment ID is compared verbatim and is digested verbatim. Digesting
|
|
22
|
+
* the raw body instead would make a case-only app ID edit change the revision
|
|
23
|
+
* without changing effective identity, which trips the inherited "revision
|
|
24
|
+
* changes iff content changes" invariant. The manifest is digested as authored,
|
|
25
|
+
* because its contents are semantic.
|
|
26
|
+
*/
|
|
27
|
+
export declare function computeOobRevision(input: {
|
|
28
|
+
environmentId: string;
|
|
29
|
+
body: OobRevisionBody;
|
|
30
|
+
}): string;
|
|
31
|
+
/** The portion of an init body the effective OOB revision is derived from. */
|
|
32
|
+
export interface OobRevisionBody {
|
|
33
|
+
app: {
|
|
34
|
+
id: string;
|
|
35
|
+
manifest: unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=OobRevision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OobRevision.d.ts","sourceRoot":"","sources":["../../src/snapshot/OobRevision.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,eAAe,CAAC;CACvB,GAAG,MAAM,CAOT;AAED,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;CACxC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic JSON with object keys sorted at every depth. Array order is
|
|
7
|
+
* preserved because it is meaningful in JSON Schema. Used only to derive the
|
|
8
|
+
* effective OOB revision, never to produce the bytes sent to the backend.
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalJson(value) {
|
|
11
|
+
return JSON.stringify(value, (_key, entry) => entry !== null && typeof entry === 'object' && !Array.isArray(entry)
|
|
12
|
+
? Object.fromEntries(Object.entries(entry).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))
|
|
13
|
+
: entry);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Opaque revision for the effective OOB content. It covers exactly what the
|
|
17
|
+
* inherited `validateDevelopmentSnapshotUpdate` treats as OOB-affecting — app
|
|
18
|
+
* identity, the inline manifest, and the effective environment identity — so it
|
|
19
|
+
* changes if and only if one of those changes. Formatting-only, asset-only and
|
|
20
|
+
* connector-only edits leave it unchanged and keep the backend session valid.
|
|
21
|
+
* Key order is insensitive because the digest is taken over canonical JSON.
|
|
22
|
+
* The digest is a local change detector, not a security or integrity claim.
|
|
23
|
+
*
|
|
24
|
+
* Identity is normalized the same way the inherited comparison normalizes it:
|
|
25
|
+
* app IDs are compared case-insensitively, so they are lowercased here, while
|
|
26
|
+
* the environment ID is compared verbatim and is digested verbatim. Digesting
|
|
27
|
+
* the raw body instead would make a case-only app ID edit change the revision
|
|
28
|
+
* without changing effective identity, which trips the inherited "revision
|
|
29
|
+
* changes iff content changes" invariant. The manifest is digested as authored,
|
|
30
|
+
* because its contents are semantic.
|
|
31
|
+
*/
|
|
32
|
+
export function computeOobRevision(input) {
|
|
33
|
+
const identity = {
|
|
34
|
+
environmentId: input.environmentId,
|
|
35
|
+
appId: input.body.app.id.toLowerCase(),
|
|
36
|
+
manifest: input.body.app.manifest,
|
|
37
|
+
};
|
|
38
|
+
return createHash('sha256').update(canonicalJson(identity), 'utf8').digest('hex').slice(0, 32);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=OobRevision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OobRevision.js","sourceRoot":"","sources":["../../src/snapshot/OobRevision.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,KAAc,EAAE,EAAE,CACpD,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAClE,CAAC,CAAC,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAC7C,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACzC,CACF;QACH,CAAC,CAAC,KAAK,CACV,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAGlC;IACC,MAAM,QAAQ,GAAG;QACf,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,WAAW,EAAE;QACtC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ;KAClC,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACjG,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Filesystem access policy for the local development snapshot reader.
|
|
6
|
+
*
|
|
7
|
+
* The reader only ever opens two kinds of file inside one explicitly supplied
|
|
8
|
+
* project root: the base app config, whose name comes from the shared
|
|
9
|
+
* `BASE_CONFIG_FILE_NAMES` list rather than from a caller, and the schema file
|
|
10
|
+
* named by `data.schemaPath` (or its legacy `db` alias). There is no upward
|
|
11
|
+
* discovery and no second allowed root.
|
|
12
|
+
*
|
|
13
|
+
* The containment rules follow `Pack`'s `loadDataSchemaAsync`
|
|
14
|
+
* (`managed-apps-cli/src/Verbs/App/Lifecycle/Pack.ts`) so a project that packs
|
|
15
|
+
* also reads: non-empty, project-relative, contained after resolution, a regular
|
|
16
|
+
* file, and valid JSON. The reader is **stricter than pack in one way**: the real
|
|
17
|
+
* path of every file it opens — base config as well as schema — must also stay
|
|
18
|
+
* inside the canonicalized root, so a link pointing outside the project is
|
|
19
|
+
* rejected here even though pack would follow it. The root itself is
|
|
20
|
+
* canonicalized first, so explicitly selecting a symlinked project directory
|
|
21
|
+
* still works. This is a deliberate incompatibility: a project that links a
|
|
22
|
+
* schema in from outside its root must copy it in to serve it locally.
|
|
23
|
+
*
|
|
24
|
+
* This is a containment policy, not a defense against concurrent modification of
|
|
25
|
+
* the project directory. Resolution and reading are separate syscalls, so a path
|
|
26
|
+
* replaced between them is not detected; the reader assumes the developer's own
|
|
27
|
+
* working tree and validates the resulting snapshot before it is published.
|
|
28
|
+
*
|
|
29
|
+
* Size is deliberately not pack's 10 MiB: initialization is limited to a 1 MiB
|
|
30
|
+
* whole UTF-8 body by `LOCAL_DEV_INIT_MAX_BODY_BYTES`, enforced through the
|
|
31
|
+
* inherited serializer, so an oversized schema fails here with a local error
|
|
32
|
+
* rather than a backend rejection.
|
|
33
|
+
*/
|
|
34
|
+
/** Reader-side failure kinds. Callers map these to their own surface. */
|
|
35
|
+
export declare const SnapshotReadErrorCode: {
|
|
36
|
+
readonly ConfigMissing: "ConfigMissing";
|
|
37
|
+
readonly ConfigPathRejected: "ConfigPathRejected";
|
|
38
|
+
readonly ConfigUnreadable: "ConfigUnreadable";
|
|
39
|
+
readonly ConfigInvalid: "ConfigInvalid";
|
|
40
|
+
readonly SchemaPathRejected: "SchemaPathRejected";
|
|
41
|
+
readonly SchemaUnreadable: "SchemaUnreadable";
|
|
42
|
+
readonly SchemaInvalid: "SchemaInvalid";
|
|
43
|
+
readonly SnapshotInvalid: "SnapshotInvalid";
|
|
44
|
+
};
|
|
45
|
+
export type SnapshotReadErrorCode = (typeof SnapshotReadErrorCode)[keyof typeof SnapshotReadErrorCode];
|
|
46
|
+
/**
|
|
47
|
+
* A read failure that never carries file bytes. The message may name the
|
|
48
|
+
* configured relative path, which the developer wrote, but not absolute
|
|
49
|
+
* locations outside the project root.
|
|
50
|
+
*/
|
|
51
|
+
export declare class SnapshotReadError extends Error {
|
|
52
|
+
readonly code: SnapshotReadErrorCode;
|
|
53
|
+
constructor(code: SnapshotReadErrorCode, message: string);
|
|
54
|
+
}
|
|
55
|
+
export declare function isPathMissingError(error: unknown): boolean;
|
|
56
|
+
/** Canonicalize the explicitly selected root so a symlinked root still works. */
|
|
57
|
+
export declare function canonicalizeRootAsync(projectRoot: string): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Assert that an already-resolved file does not link out of `canonicalRoot`,
|
|
60
|
+
* which must come from {@link canonicalizeRootAsync}.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertRealPathContainedAsync(canonicalRoot: string, resolved: string, rejectedCode: SnapshotReadErrorCode, describe: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a configured schema path against the canonicalized project root,
|
|
65
|
+
* rejecting anything that is absolute, escapes the root lexically or through a
|
|
66
|
+
* link, or is not a regular file. Returns an absolute path safe to read.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveSchemaPathAsync(canonicalRoot: string, schemaPath: string, configKey: 'data' | 'db'): Promise<string>;
|
|
69
|
+
//# sourceMappingURL=PathPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PathPolicy.d.ts","sourceRoot":"","sources":["../../src/snapshot/PathPolicy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,yEAAyE;AACzE,eAAO,MAAM,qBAAqB;;;;;;;;;CASxB,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,OAAO,qBAAqB,CAAC,CAAC;AAErE;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAgB,IAAI,EAAE,qBAAqB,CAAC;gBAEhC,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM;CAKzD;AAYD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,iFAAiF;AACjF,wBAAsB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahF;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,qBAAqB,EACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,MAAM,CAAC,CA+CjB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fsp } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Filesystem access policy for the local development snapshot reader.
|
|
8
|
+
*
|
|
9
|
+
* The reader only ever opens two kinds of file inside one explicitly supplied
|
|
10
|
+
* project root: the base app config, whose name comes from the shared
|
|
11
|
+
* `BASE_CONFIG_FILE_NAMES` list rather than from a caller, and the schema file
|
|
12
|
+
* named by `data.schemaPath` (or its legacy `db` alias). There is no upward
|
|
13
|
+
* discovery and no second allowed root.
|
|
14
|
+
*
|
|
15
|
+
* The containment rules follow `Pack`'s `loadDataSchemaAsync`
|
|
16
|
+
* (`managed-apps-cli/src/Verbs/App/Lifecycle/Pack.ts`) so a project that packs
|
|
17
|
+
* also reads: non-empty, project-relative, contained after resolution, a regular
|
|
18
|
+
* file, and valid JSON. The reader is **stricter than pack in one way**: the real
|
|
19
|
+
* path of every file it opens — base config as well as schema — must also stay
|
|
20
|
+
* inside the canonicalized root, so a link pointing outside the project is
|
|
21
|
+
* rejected here even though pack would follow it. The root itself is
|
|
22
|
+
* canonicalized first, so explicitly selecting a symlinked project directory
|
|
23
|
+
* still works. This is a deliberate incompatibility: a project that links a
|
|
24
|
+
* schema in from outside its root must copy it in to serve it locally.
|
|
25
|
+
*
|
|
26
|
+
* This is a containment policy, not a defense against concurrent modification of
|
|
27
|
+
* the project directory. Resolution and reading are separate syscalls, so a path
|
|
28
|
+
* replaced between them is not detected; the reader assumes the developer's own
|
|
29
|
+
* working tree and validates the resulting snapshot before it is published.
|
|
30
|
+
*
|
|
31
|
+
* Size is deliberately not pack's 10 MiB: initialization is limited to a 1 MiB
|
|
32
|
+
* whole UTF-8 body by `LOCAL_DEV_INIT_MAX_BODY_BYTES`, enforced through the
|
|
33
|
+
* inherited serializer, so an oversized schema fails here with a local error
|
|
34
|
+
* rather than a backend rejection.
|
|
35
|
+
*/
|
|
36
|
+
/** Reader-side failure kinds. Callers map these to their own surface. */
|
|
37
|
+
export const SnapshotReadErrorCode = {
|
|
38
|
+
ConfigMissing: 'ConfigMissing',
|
|
39
|
+
ConfigPathRejected: 'ConfigPathRejected',
|
|
40
|
+
ConfigUnreadable: 'ConfigUnreadable',
|
|
41
|
+
ConfigInvalid: 'ConfigInvalid',
|
|
42
|
+
SchemaPathRejected: 'SchemaPathRejected',
|
|
43
|
+
SchemaUnreadable: 'SchemaUnreadable',
|
|
44
|
+
SchemaInvalid: 'SchemaInvalid',
|
|
45
|
+
SnapshotInvalid: 'SnapshotInvalid',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A read failure that never carries file bytes. The message may name the
|
|
49
|
+
* configured relative path, which the developer wrote, but not absolute
|
|
50
|
+
* locations outside the project root.
|
|
51
|
+
*/
|
|
52
|
+
export class SnapshotReadError extends Error {
|
|
53
|
+
code;
|
|
54
|
+
constructor(code, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'SnapshotReadError';
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isContained(root, resolved) {
|
|
61
|
+
const relative = path.relative(root, resolved);
|
|
62
|
+
return (relative !== '' &&
|
|
63
|
+
relative !== '..' &&
|
|
64
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
65
|
+
!path.isAbsolute(relative));
|
|
66
|
+
}
|
|
67
|
+
export function isPathMissingError(error) {
|
|
68
|
+
return error.code === 'ENOENT';
|
|
69
|
+
}
|
|
70
|
+
/** Canonicalize the explicitly selected root so a symlinked root still works. */
|
|
71
|
+
export async function canonicalizeRootAsync(projectRoot) {
|
|
72
|
+
const resolved = path.resolve(projectRoot);
|
|
73
|
+
try {
|
|
74
|
+
return await fsp.realpath(resolved);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isPathMissingError(error)) {
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.ConfigUnreadable, `Unable to resolve the project root: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Assert that an already-resolved file does not link out of `canonicalRoot`,
|
|
85
|
+
* which must come from {@link canonicalizeRootAsync}.
|
|
86
|
+
*/
|
|
87
|
+
export async function assertRealPathContainedAsync(canonicalRoot, resolved, rejectedCode, describe) {
|
|
88
|
+
let real;
|
|
89
|
+
try {
|
|
90
|
+
real = await fsp.realpath(resolved);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new SnapshotReadError(rejectedCode, `Unable to resolve ${describe}: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
if (!isContained(canonicalRoot, real)) {
|
|
96
|
+
throw new SnapshotReadError(rejectedCode, `${describe} must not link outside the project root`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a configured schema path against the canonicalized project root,
|
|
101
|
+
* rejecting anything that is absolute, escapes the root lexically or through a
|
|
102
|
+
* link, or is not a regular file. Returns an absolute path safe to read.
|
|
103
|
+
*/
|
|
104
|
+
export async function resolveSchemaPathAsync(canonicalRoot, schemaPath, configKey) {
|
|
105
|
+
const setting = `${configKey}.schemaPath`;
|
|
106
|
+
if (!schemaPath.trim()) {
|
|
107
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaPathRejected, `${setting} must not be empty`);
|
|
108
|
+
}
|
|
109
|
+
if (path.isAbsolute(schemaPath)) {
|
|
110
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaPathRejected, `${setting} must be relative to the project root: "${schemaPath}"`);
|
|
111
|
+
}
|
|
112
|
+
const resolved = path.resolve(canonicalRoot, schemaPath);
|
|
113
|
+
if (!isContained(canonicalRoot, resolved)) {
|
|
114
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaPathRejected, `${setting} must not resolve outside the project root: "${schemaPath}"`);
|
|
115
|
+
}
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = await fsp.stat(resolved);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaUnreadable, `Unable to access ${setting} "${schemaPath}": ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
if (!stat.isFile()) {
|
|
124
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaPathRejected, `${setting} must reference a regular file: "${schemaPath}"`);
|
|
125
|
+
}
|
|
126
|
+
await assertRealPathContainedAsync(canonicalRoot, resolved, SnapshotReadErrorCode.SchemaPathRejected, `${setting} "${schemaPath}"`);
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=PathPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PathPolicy.js","sourceRoot":"","sources":["../../src/snapshot/PathPolicy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import type { DevelopmentSnapshot } from '@microsoft/managed-apps-common/gateway';
|
|
5
|
+
/**
|
|
6
|
+
* Filesystem half of `@microsoft/managed-apps-dev-gateway`: it turns the project's
|
|
7
|
+
* on-disk config and schema into one coherent, immutable `DevelopmentSnapshot`.
|
|
8
|
+
*
|
|
9
|
+
* It is a pure reader. There is no HTTP listener, endpoint handler, watcher, Vite
|
|
10
|
+
* integration, browser channel handler or remote authorization here, and it never
|
|
11
|
+
* builds or packs the customer app — `manifest.json` is not required on disk.
|
|
12
|
+
* A snapshot is configuration, never proof of authorization.
|
|
13
|
+
*
|
|
14
|
+
* **Exposure boundary.** The reader reads only the validated `ConfigSchemaV0`
|
|
15
|
+
* fields of the project's base config plus the JSON schema file that config
|
|
16
|
+
* points at. It reads no credentials, tokens or other files, and adds no fields
|
|
17
|
+
* of its own. That is a bound on what it collects, not a claim that the result is
|
|
18
|
+
* harmless: the inline schema is data the legacy config endpoint never carried,
|
|
19
|
+
* and an app config can hold user-authored content the developer considers
|
|
20
|
+
* sensitive. Whoever exposes a snapshot owns that decision; the HTTP exposure
|
|
21
|
+
* policy is not settled here.
|
|
22
|
+
*/
|
|
23
|
+
/** Reader inputs. The root is explicit; the reader never discovers one itself. */
|
|
24
|
+
export interface ReadDevelopmentSnapshotOptions {
|
|
25
|
+
/** Absolute path of the project directory whose config may be served. */
|
|
26
|
+
projectRoot: string;
|
|
27
|
+
/**
|
|
28
|
+
* Combined snapshot revision for this read, owned by the caller because it is
|
|
29
|
+
* scoped to a bootstrap lifetime the reader does not observe. Must increase
|
|
30
|
+
* within one bootstrap; a restarted producer starts a new bootstrap instead.
|
|
31
|
+
*/
|
|
32
|
+
revision: number;
|
|
33
|
+
}
|
|
34
|
+
/** Successful read plus the exact init bytes retained for retries. */
|
|
35
|
+
export interface DevelopmentSnapshotRead {
|
|
36
|
+
snapshot: DevelopmentSnapshot;
|
|
37
|
+
/**
|
|
38
|
+
* The serialized `InitDevSessionBody` that was validated against the 1 MiB
|
|
39
|
+
* limit. Retain these bytes with the candidate GUID so a retry replays the
|
|
40
|
+
* identical request rather than re-serializing.
|
|
41
|
+
*/
|
|
42
|
+
initBody: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Freeze a JSON value in place at every depth. `Object.freeze` is shallow, so a
|
|
46
|
+
* shallowly frozen snapshot still lets a caller mutate `hostConfig.appId`,
|
|
47
|
+
* `oob.environmentId` or `oob.body.app.manifest.data`. Because the source stores
|
|
48
|
+
* and hands back the same object, such a mutation would corrupt retained state
|
|
49
|
+
* and desynchronize the snapshot from its already-serialized init body.
|
|
50
|
+
*
|
|
51
|
+
* The value is known-acyclic here: it comes from `JSON.parse` and object
|
|
52
|
+
* literals, then through a Zod parse, so no cycle tracking is needed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function deepFreezeJson<T>(value: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Produce one coherent snapshot from the project's current files. Both portions
|
|
57
|
+
* derive from a single config read, so the host config and the OOB body can never
|
|
58
|
+
* describe different apps. The OOB body carries app identity and the inline
|
|
59
|
+
* manifest only: connection references, connections and deployment fields stay in
|
|
60
|
+
* the host config and are never sent to initialization.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readDevelopmentSnapshotAsync(options: ReadDevelopmentSnapshotOptions): Promise<DevelopmentSnapshotRead>;
|
|
63
|
+
//# sourceMappingURL=SnapshotReader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SnapshotReader.d.ts","sourceRoot":"","sources":["../../src/snapshot/SnapshotReader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAkBlF;;;;;;;;;;;;;;;;;GAiBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,8BAA8B;IAC7C,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,sEAAsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoKD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAI7C;AAED;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAwDlC"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fsp } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { BASE_CONFIG_FILE_NAMES, ConfigSchemaV0 } from '@microsoft/managed-apps-common/config';
|
|
7
|
+
import { DevelopmentSnapshotSchema, LOCAL_DEV_INIT_MAX_BODY_BYTES, LOCAL_DEV_OOB_MANIFEST_VERSION, serializeInitDevSessionBody, } from '@microsoft/managed-apps-common/gateway';
|
|
8
|
+
import { computeOobRevision } from './OobRevision.js';
|
|
9
|
+
import { assertRealPathContainedAsync, canonicalizeRootAsync, isPathMissingError, resolveSchemaPathAsync, SnapshotReadError, SnapshotReadErrorCode, } from './PathPolicy.js';
|
|
10
|
+
const CONFIG_KEYS = ['data', 'db'];
|
|
11
|
+
const CONFIG_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
async function readBoundedUtf8FileAsync(options) {
|
|
13
|
+
let stat;
|
|
14
|
+
try {
|
|
15
|
+
stat = await fsp.stat(options.filePath);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new SnapshotReadError(options.unreadableCode, `Unable to access ${options.displayName}: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
if (stat.size > options.maxBytes) {
|
|
21
|
+
throw new SnapshotReadError(options.invalidCode, `${options.displayName} exceeds the ${options.maxBytes}-byte file limit`);
|
|
22
|
+
}
|
|
23
|
+
let content;
|
|
24
|
+
try {
|
|
25
|
+
content = await fsp.readFile(options.filePath, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new SnapshotReadError(options.unreadableCode, `Unable to read ${options.displayName}: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
if (Buffer.byteLength(content, 'utf-8') > options.maxBytes) {
|
|
31
|
+
throw new SnapshotReadError(options.invalidCode, `${options.displayName} exceeds the ${options.maxBytes}-byte file limit`);
|
|
32
|
+
}
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
async function readBaseConfigAsync(canonicalRoot) {
|
|
36
|
+
const attempted = [];
|
|
37
|
+
for (const fileName of BASE_CONFIG_FILE_NAMES) {
|
|
38
|
+
attempted.push(fileName);
|
|
39
|
+
const filePath = path.join(canonicalRoot, fileName);
|
|
40
|
+
let stat;
|
|
41
|
+
try {
|
|
42
|
+
stat = await fsp.stat(filePath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isPathMissingError(error)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.ConfigUnreadable, `Unable to access ${fileName}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
if (!stat?.isFile()) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
await assertRealPathContainedAsync(canonicalRoot, filePath, SnapshotReadErrorCode.ConfigPathRejected, fileName);
|
|
54
|
+
const content = await readBoundedUtf8FileAsync({
|
|
55
|
+
filePath,
|
|
56
|
+
displayName: fileName,
|
|
57
|
+
maxBytes: CONFIG_MAX_FILE_BYTES,
|
|
58
|
+
unreadableCode: SnapshotReadErrorCode.ConfigUnreadable,
|
|
59
|
+
invalidCode: SnapshotReadErrorCode.ConfigInvalid,
|
|
60
|
+
});
|
|
61
|
+
return { fileName, content };
|
|
62
|
+
}
|
|
63
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.ConfigMissing, `No app config found in the project root. Expected one of: ${attempted.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
function parseConfig(fileName, content) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(content);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.ConfigInvalid, `${fileName} must contain valid JSON: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
const result = ConfigSchemaV0.safeParse(parsed);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
const issues = result.error.issues
|
|
76
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
|
|
77
|
+
.join('\n');
|
|
78
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.ConfigInvalid, `${fileName} is not a valid app config:\n${issues}`);
|
|
79
|
+
}
|
|
80
|
+
return result.data;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read the configured schema, if any. Returns `undefined` only when no schema is
|
|
84
|
+
* configured at all; a configured-but-broken schema is an error, never an empty
|
|
85
|
+
* manifest, so invalid input can never silently replace a valid snapshot.
|
|
86
|
+
*/
|
|
87
|
+
async function readInlineSchemaAsync(canonicalRoot, appConfig) {
|
|
88
|
+
const configKey = CONFIG_KEYS.find((key) => appConfig[key] !== undefined);
|
|
89
|
+
if (configKey === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const schemaPath = appConfig[configKey].schemaPath;
|
|
93
|
+
const resolved = await resolveSchemaPathAsync(canonicalRoot, schemaPath, configKey);
|
|
94
|
+
const content = await readBoundedUtf8FileAsync({
|
|
95
|
+
filePath: resolved,
|
|
96
|
+
displayName: `${configKey}.schemaPath "${schemaPath}"`,
|
|
97
|
+
maxBytes: LOCAL_DEV_INIT_MAX_BODY_BYTES,
|
|
98
|
+
unreadableCode: SnapshotReadErrorCode.SchemaUnreadable,
|
|
99
|
+
invalidCode: SnapshotReadErrorCode.SchemaInvalid,
|
|
100
|
+
});
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(content);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaInvalid, `${configKey}.schemaPath "${schemaPath}" must contain valid JSON: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
109
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SchemaInvalid, `${configKey}.schemaPath "${schemaPath}" must contain a JSON object`);
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Freeze a JSON value in place at every depth. `Object.freeze` is shallow, so a
|
|
115
|
+
* shallowly frozen snapshot still lets a caller mutate `hostConfig.appId`,
|
|
116
|
+
* `oob.environmentId` or `oob.body.app.manifest.data`. Because the source stores
|
|
117
|
+
* and hands back the same object, such a mutation would corrupt retained state
|
|
118
|
+
* and desynchronize the snapshot from its already-serialized init body.
|
|
119
|
+
*
|
|
120
|
+
* The value is known-acyclic here: it comes from `JSON.parse` and object
|
|
121
|
+
* literals, then through a Zod parse, so no cycle tracking is needed.
|
|
122
|
+
*/
|
|
123
|
+
export function deepFreezeJson(value) {
|
|
124
|
+
if (value === null || typeof value !== 'object' || Object.isFrozen(value))
|
|
125
|
+
return value;
|
|
126
|
+
for (const entry of Object.values(value))
|
|
127
|
+
deepFreezeJson(entry);
|
|
128
|
+
return Object.freeze(value);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Produce one coherent snapshot from the project's current files. Both portions
|
|
132
|
+
* derive from a single config read, so the host config and the OOB body can never
|
|
133
|
+
* describe different apps. The OOB body carries app identity and the inline
|
|
134
|
+
* manifest only: connection references, connections and deployment fields stay in
|
|
135
|
+
* the host config and are never sent to initialization.
|
|
136
|
+
*/
|
|
137
|
+
export async function readDevelopmentSnapshotAsync(options) {
|
|
138
|
+
const canonicalRoot = await canonicalizeRootAsync(options.projectRoot);
|
|
139
|
+
const { fileName, content } = await readBaseConfigAsync(canonicalRoot);
|
|
140
|
+
const hostConfig = parseConfig(fileName, content);
|
|
141
|
+
const schema = await readInlineSchemaAsync(canonicalRoot, hostConfig);
|
|
142
|
+
const dataEnabled = hostConfig.data?.enabled ?? hostConfig.db?.enabled;
|
|
143
|
+
const body = {
|
|
144
|
+
app: {
|
|
145
|
+
id: hostConfig.appId ?? '',
|
|
146
|
+
manifest: {
|
|
147
|
+
manifestVersion: LOCAL_DEV_OOB_MANIFEST_VERSION,
|
|
148
|
+
...(dataEnabled !== undefined ? { dataEnabled } : {}),
|
|
149
|
+
...(schema !== undefined ? { data: schema } : {}),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const environmentId = hostConfig.environmentId ?? '';
|
|
154
|
+
let initBody;
|
|
155
|
+
try {
|
|
156
|
+
// Also enforces the 1 MiB whole-body limit before anything is published.
|
|
157
|
+
initBody = serializeInitDevSessionBody(body);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SnapshotInvalid, `${fileName} does not describe a usable development session: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
const candidate = {
|
|
163
|
+
version: 1,
|
|
164
|
+
revision: options.revision,
|
|
165
|
+
hostConfig,
|
|
166
|
+
oob: {
|
|
167
|
+
version: 1,
|
|
168
|
+
revision: computeOobRevision({ environmentId, body }),
|
|
169
|
+
environmentId,
|
|
170
|
+
body,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const validated = DevelopmentSnapshotSchema.safeParse(candidate);
|
|
174
|
+
if (!validated.success) {
|
|
175
|
+
const issues = validated.error.issues
|
|
176
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
|
|
177
|
+
.join('\n');
|
|
178
|
+
throw new SnapshotReadError(SnapshotReadErrorCode.SnapshotInvalid, `${fileName} does not describe a usable development session:\n${issues}`);
|
|
179
|
+
}
|
|
180
|
+
return { snapshot: deepFreezeJson(validated.data), initBody };
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=SnapshotReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SnapshotReader.js","sourceRoot":"","sources":["../../src/snapshot/SnapshotReader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import type { DevelopmentSnapshot } from '@microsoft/managed-apps-common/gateway';
|
|
5
|
+
import { validateDevelopmentSnapshotUpdate } from '@microsoft/managed-apps-common/gateway';
|
|
6
|
+
import type { DevelopmentSnapshotRead } from './SnapshotReader.js';
|
|
7
|
+
/**
|
|
8
|
+
* Holds the last snapshot that read and validated cleanly, and mints the combined
|
|
9
|
+
* revision for one bootstrap lifetime.
|
|
10
|
+
*
|
|
11
|
+
* A failed refresh leaves the previous snapshot installed and rethrows, so invalid
|
|
12
|
+
* input can never downgrade a working session to an empty manifest. This is state
|
|
13
|
+
* for a caller that decides *when* to refresh; it is not a watcher, debouncer or
|
|
14
|
+
* session coordinator, and it performs no HTTP work.
|
|
15
|
+
*/
|
|
16
|
+
export declare class DevelopmentSnapshotSource {
|
|
17
|
+
private readonly projectRoot;
|
|
18
|
+
private revision;
|
|
19
|
+
private current;
|
|
20
|
+
/**
|
|
21
|
+
* Tail of the serialized refresh queue. Refreshes must not interleave: the
|
|
22
|
+
* combined revision is allocated from `this.revision`, and an `await` between
|
|
23
|
+
* allocation and installation would let two overlapping refreshes both mint
|
|
24
|
+
* `N + 1` and hand back distinct snapshots carrying the same revision. Each
|
|
25
|
+
* caller installs a fulfilled gate before awaiting the previous one, preserving
|
|
26
|
+
* FIFO order without letting a failed refresh poison the callers behind it.
|
|
27
|
+
*/
|
|
28
|
+
private pending;
|
|
29
|
+
constructor(projectRoot: string);
|
|
30
|
+
/** The last successful read, or `undefined` before the first one. */
|
|
31
|
+
get snapshot(): DevelopmentSnapshot | undefined;
|
|
32
|
+
/** Init bytes matching {@link snapshot}, retained for exact-body retries. */
|
|
33
|
+
get initBody(): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Re-read the project. On success the new snapshot is installed and classified
|
|
36
|
+
* relative to the previous one, so a caller can tell a connector-only metadata
|
|
37
|
+
* update from an OOB or identity change. On failure the previous snapshot stays
|
|
38
|
+
* installed and the error propagates.
|
|
39
|
+
*
|
|
40
|
+
* Concurrent calls are serialized, not coalesced: every call performs its own
|
|
41
|
+
* read, in call order, and observes the state left by the one before it.
|
|
42
|
+
*/
|
|
43
|
+
refreshAsync(): Promise<{
|
|
44
|
+
read: DevelopmentSnapshotRead;
|
|
45
|
+
change: ReturnType<typeof validateDevelopmentSnapshotUpdate>['change'];
|
|
46
|
+
}>;
|
|
47
|
+
private readAndInstallAsync;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=SnapshotSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SnapshotSource.d.ts","sourceRoot":"","sources":["../../src/snapshot/SnapshotSource.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,iCAAiC,EAAE,MAAM,wCAAwC,CAAC;AAE3F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGhE;;;;;;;;GAQG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,OAAO,CAAsC;IACrD;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO,CAAoC;gBAEvC,WAAW,EAAE,MAAM;IAI/B,qEAAqE;IACrE,IAAW,QAAQ,IAAI,mBAAmB,GAAG,SAAS,CAErD;IAED,6EAA6E;IAC7E,IAAW,QAAQ,IAAI,MAAM,GAAG,SAAS,CAExC;IAED;;;;;;;;OAQG;IACU,YAAY,IAAI,OAAO,CAAC;QACnC,IAAI,EAAE,uBAAuB,CAAC;QAC9B,MAAM,EAAE,UAAU,CAAC,OAAO,iCAAiC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACxE,CAAC;YAeY,mBAAmB;CAyBlC"}
|