@serve.zone/interfaces 32.1.0 → 32.2.0
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/changelog.md +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/platform/storage.d.ts +29 -2
- package/dist_ts/platform/storage.golden.d.ts +31 -0
- package/dist_ts/platform/storage.golden.js +84 -0
- package/dist_ts/platform/storage.js +37 -1
- package/dist_ts/platform/storagemigration.js +11 -287
- package/dist_ts/private/storagecontract.d.ts +40 -0
- package/dist_ts/private/storagecontract.js +399 -0
- package/package.json +1 -1
- package/readme.md +15 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/platform/storage.golden.ts +97 -0
- package/ts/platform/storage.ts +59 -2
- package/ts/platform/storagemigration.ts +31 -440
- package/ts/private/storagecontract.ts +623 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-18 - 32.2.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add `platform.createStorageRequestSha256(value)` and `platform.canonicalizeStorageRequest(value)`,
|
|
8
|
+
the contract's own statement of `IResolvedStorageBindingBase.requestDigest`: the request alone,
|
|
9
|
+
normalized to its contract shape, written as strict canonical JSON and hashed to bare lowercase
|
|
10
|
+
hex. A control plane issues that value on a binding and its consumer re-derives it, so both now
|
|
11
|
+
call one function instead of each deriving a value the other cannot reproduce — a prefix or a
|
|
12
|
+
wrapper object around the request is a different value for the same binding. Anything that is not
|
|
13
|
+
a storage request is refused with the new `platform.StorageContractError` rather than hashed.
|
|
14
|
+
`platform.storageRequestCanonicalDigestGoldenVectors` writes the digest down for a filesystem
|
|
15
|
+
request and for both object-storage deliveries.
|
|
16
|
+
- The storage request normalizer is now one implementation for both kinds, shared by
|
|
17
|
+
`platform.storage` and `platform.storagemigration`, so
|
|
18
|
+
`createObjectStorageMigrationTargetRequestSha256` and `createStorageRequestSha256` cannot drift.
|
|
19
|
+
Migration contract errors keep their own class and messages.
|
|
20
|
+
|
|
3
21
|
## 2026-09-18 - 32.1.0
|
|
4
22
|
|
|
5
23
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '32.
|
|
6
|
+
version: '32.2.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
|
@@ -142,8 +142,12 @@ export interface IResolvedStorageBindingBase {
|
|
|
142
142
|
/** Stable App Store storageRequests[].id. */
|
|
143
143
|
requestId: string;
|
|
144
144
|
/**
|
|
145
|
-
* Canonical digest of the normalized request
|
|
146
|
-
*
|
|
145
|
+
* Canonical digest of the normalized request, exactly as
|
|
146
|
+
* `createStorageRequestSha256` states it: bare lowercase hex over the request
|
|
147
|
+
* alone. Adapters use it to distinguish an idempotent reconciliation from a
|
|
148
|
+
* migration, and a consumer re-derives it to recognise the request it asked
|
|
149
|
+
* for, so the control plane and the consumer call that one function rather
|
|
150
|
+
* than each deriving a value the other cannot reproduce.
|
|
147
151
|
*/
|
|
148
152
|
requestDigest: string;
|
|
149
153
|
kind: TStorageResourceKind;
|
|
@@ -180,3 +184,26 @@ export interface IResolvedObjectStorageBinding extends IResolvedStorageBindingBa
|
|
|
180
184
|
retentionDays?: number;
|
|
181
185
|
}
|
|
182
186
|
export type TResolvedStorageBinding = IResolvedFilesystemStorageBinding | IResolvedObjectStorageBinding;
|
|
187
|
+
export declare class StorageContractError extends Error {
|
|
188
|
+
constructor(reasonArg: string);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The exact bytes a storage request's digest is taken over: the request normalized to its contract
|
|
192
|
+
* shape, written as strict canonical JSON — keys in UTF-16 code-unit order, no whitespace, no
|
|
193
|
+
* absent member carried as `null`.
|
|
194
|
+
*
|
|
195
|
+
* Anything that is not a storage request is refused rather than hashed, because a digest of an
|
|
196
|
+
* unjudged body would name a request no adapter could fulfil.
|
|
197
|
+
*/
|
|
198
|
+
export declare const canonicalizeStorageRequest: (valueArg: unknown) => string;
|
|
199
|
+
/**
|
|
200
|
+
* The storage request digest: bare lowercase hex SHA-256 over the canonical request, with no
|
|
201
|
+
* prefix and nothing but the request in the hashed value.
|
|
202
|
+
*
|
|
203
|
+
* A control plane issues this value on a binding and a consumer re-derives it from the request it
|
|
204
|
+
* declared; the two only agree because they call this one function. A second derivation elsewhere
|
|
205
|
+
* — a prefix, or a wrapper object around the request — is a different value for the same binding,
|
|
206
|
+
* and the consumer would keep rejecting a binding that is in fact its own.
|
|
207
|
+
*/
|
|
208
|
+
export declare const createStorageRequestSha256: (valueArg: unknown) => Promise<string>;
|
|
209
|
+
export { storageRequestCanonicalDigestGoldenVectors, type IStorageRequestCanonicalDigestGoldenVector, } from './storage.golden.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IAppStoreFilesystemStorageRequest, IAppStoreObjectStorageRequest } from '../appstore/types.js';
|
|
2
|
+
export interface IStorageRequestCanonicalDigestGoldenVector<TInput> {
|
|
3
|
+
input: TInput;
|
|
4
|
+
canonicalJson: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The storage request digest, written down for both kinds and both deliveries.
|
|
9
|
+
*
|
|
10
|
+
* A control plane and its consumer compare this value, so it is stated here as bytes rather than
|
|
11
|
+
* as whatever the current normalizer happens to produce: a member that starts or stops
|
|
12
|
+
* participating, an optional member that starts being carried when it is absent, or a delivery
|
|
13
|
+
* mode written as anything but its number would move these digests and be caught here.
|
|
14
|
+
*/
|
|
15
|
+
export declare const storageRequestCanonicalDigestGoldenVectors: {
|
|
16
|
+
filesystem: {
|
|
17
|
+
input: IAppStoreFilesystemStorageRequest;
|
|
18
|
+
canonicalJson: string;
|
|
19
|
+
sha256: string;
|
|
20
|
+
};
|
|
21
|
+
objectStorageLauncherEnvironment: {
|
|
22
|
+
input: IAppStoreObjectStorageRequest;
|
|
23
|
+
canonicalJson: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
};
|
|
26
|
+
objectStorageFile: {
|
|
27
|
+
input: IAppStoreObjectStorageRequest;
|
|
28
|
+
canonicalJson: string;
|
|
29
|
+
sha256: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { deepFreezeValue } from '../private/canonicaljson.js';
|
|
2
|
+
const filesystemRequest = {
|
|
3
|
+
id: 'app-state',
|
|
4
|
+
kind: 'filesystem',
|
|
5
|
+
storageClass: 'databaseFast',
|
|
6
|
+
accessMode: 'ReadWriteOnce',
|
|
7
|
+
reclaimPolicy: 'delete',
|
|
8
|
+
mountPath: '/var/lib/app',
|
|
9
|
+
capacity: {
|
|
10
|
+
request: '20GiB',
|
|
11
|
+
limit: '40GiB',
|
|
12
|
+
},
|
|
13
|
+
protection: {
|
|
14
|
+
backup: 'required',
|
|
15
|
+
snapshots: 'portable',
|
|
16
|
+
consistency: 'applicationConsistent',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const objectStorageLauncherEnvironmentRequest = {
|
|
20
|
+
id: 'object-data',
|
|
21
|
+
kind: 'objectStorage',
|
|
22
|
+
storageClass: 'backupCapacity',
|
|
23
|
+
accessMode: 'readWrite',
|
|
24
|
+
reclaimPolicy: 'retain',
|
|
25
|
+
delivery: {
|
|
26
|
+
type: 'launcher-environment',
|
|
27
|
+
keys: {
|
|
28
|
+
endpoint: 'APP_S3_ENDPOINT',
|
|
29
|
+
bucket: 'APP_S3_BUCKET',
|
|
30
|
+
region: 'APP_S3_REGION',
|
|
31
|
+
accessKeyId: 'APP_S3_ACCESS_KEY',
|
|
32
|
+
secretAccessKey: 'APP_S3_SECRET_KEY',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
capacity: {
|
|
36
|
+
request: '500GiB',
|
|
37
|
+
},
|
|
38
|
+
protection: {
|
|
39
|
+
backup: 'required',
|
|
40
|
+
versioning: 'required',
|
|
41
|
+
retentionDays: 30,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const objectStorageFileRequest = {
|
|
45
|
+
id: 'object-archive',
|
|
46
|
+
kind: 'objectStorage',
|
|
47
|
+
storageClass: 'archiveCapacity',
|
|
48
|
+
accessMode: 'readOnly',
|
|
49
|
+
reclaimPolicy: 'delete',
|
|
50
|
+
delivery: {
|
|
51
|
+
type: 'file',
|
|
52
|
+
targetPath: '/run/secrets/object-archive.json',
|
|
53
|
+
format: 'servezone-object-storage',
|
|
54
|
+
uid: 10_000,
|
|
55
|
+
gid: 10_000,
|
|
56
|
+
mode: 0o400,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The storage request digest, written down for both kinds and both deliveries.
|
|
61
|
+
*
|
|
62
|
+
* A control plane and its consumer compare this value, so it is stated here as bytes rather than
|
|
63
|
+
* as whatever the current normalizer happens to produce: a member that starts or stops
|
|
64
|
+
* participating, an optional member that starts being carried when it is absent, or a delivery
|
|
65
|
+
* mode written as anything but its number would move these digests and be caught here.
|
|
66
|
+
*/
|
|
67
|
+
export const storageRequestCanonicalDigestGoldenVectors = deepFreezeValue({
|
|
68
|
+
filesystem: {
|
|
69
|
+
input: filesystemRequest,
|
|
70
|
+
canonicalJson: '{"accessMode":"ReadWriteOnce","capacity":{"limit":"40GiB","request":"20GiB"},"id":"app-state","kind":"filesystem","mountPath":"/var/lib/app","protection":{"backup":"required","consistency":"applicationConsistent","snapshots":"portable"},"reclaimPolicy":"delete","storageClass":"databaseFast"}',
|
|
71
|
+
sha256: '0f8dc2e480ade7110be1f1d95f666cc56d5b90110d1c4a1b8b8383e2eeda6fae',
|
|
72
|
+
},
|
|
73
|
+
objectStorageLauncherEnvironment: {
|
|
74
|
+
input: objectStorageLauncherEnvironmentRequest,
|
|
75
|
+
canonicalJson: '{"accessMode":"readWrite","capacity":{"request":"500GiB"},"delivery":{"keys":{"accessKeyId":"APP_S3_ACCESS_KEY","bucket":"APP_S3_BUCKET","endpoint":"APP_S3_ENDPOINT","region":"APP_S3_REGION","secretAccessKey":"APP_S3_SECRET_KEY"},"type":"launcher-environment"},"id":"object-data","kind":"objectStorage","protection":{"backup":"required","retentionDays":30,"versioning":"required"},"reclaimPolicy":"retain","storageClass":"backupCapacity"}',
|
|
76
|
+
sha256: '88bd943800bea75b576612b966484e66ddbf7ec9529a7f29dd1eadc9a36e2b79',
|
|
77
|
+
},
|
|
78
|
+
objectStorageFile: {
|
|
79
|
+
input: objectStorageFileRequest,
|
|
80
|
+
canonicalJson: '{"accessMode":"readOnly","delivery":{"format":"servezone-object-storage","gid":10000,"mode":256,"targetPath":"/run/secrets/object-archive.json","type":"file","uid":10000},"id":"object-archive","kind":"objectStorage","reclaimPolicy":"delete","storageClass":"archiveCapacity"}',
|
|
81
|
+
sha256: '13186abbfe733b3917363e0694d367f2d6aa87a4064445532d7c857ee274f795',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS5nb2xkZW4uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9wbGF0Zm9ybS9zdG9yYWdlLmdvbGRlbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFJQSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFROUQsTUFBTSxpQkFBaUIsR0FBc0M7SUFDM0QsRUFBRSxFQUFFLFdBQVc7SUFDZixJQUFJLEVBQUUsWUFBWTtJQUNsQixZQUFZLEVBQUUsY0FBYztJQUM1QixVQUFVLEVBQUUsZUFBZTtJQUMzQixhQUFhLEVBQUUsUUFBUTtJQUN2QixTQUFTLEVBQUUsY0FBYztJQUN6QixRQUFRLEVBQUU7UUFDUixPQUFPLEVBQUUsT0FBTztRQUNoQixLQUFLLEVBQUUsT0FBTztLQUNmO0lBQ0QsVUFBVSxFQUFFO1FBQ1YsTUFBTSxFQUFFLFVBQVU7UUFDbEIsU0FBUyxFQUFFLFVBQVU7UUFDckIsV0FBVyxFQUFFLHVCQUF1QjtLQUNyQztDQUNGLENBQUM7QUFFRixNQUFNLHVDQUF1QyxHQUFrQztJQUM3RSxFQUFFLEVBQUUsYUFBYTtJQUNqQixJQUFJLEVBQUUsZUFBZTtJQUNyQixZQUFZLEVBQUUsZ0JBQWdCO0lBQzlCLFVBQVUsRUFBRSxXQUFXO0lBQ3ZCLGFBQWEsRUFBRSxRQUFRO0lBQ3ZCLFFBQVEsRUFBRTtRQUNSLElBQUksRUFBRSxzQkFBc0I7UUFDNUIsSUFBSSxFQUFFO1lBQ0osUUFBUSxFQUFFLGlCQUFpQjtZQUMzQixNQUFNLEVBQUUsZUFBZTtZQUN2QixNQUFNLEVBQUUsZUFBZTtZQUN2QixXQUFXLEVBQUUsbUJBQW1CO1lBQ2hDLGVBQWUsRUFBRSxtQkFBbUI7U0FDckM7S0FDRjtJQUNELFFBQVEsRUFBRTtRQUNSLE9BQU8sRUFBRSxRQUFRO0tBQ2xCO0lBQ0QsVUFBVSxFQUFFO1FBQ1YsTUFBTSxFQUFFLFVBQVU7UUFDbEIsVUFBVSxFQUFFLFVBQVU7UUFDdEIsYUFBYSxFQUFFLEVBQUU7S0FDbEI7Q0FDRixDQUFDO0FBRUYsTUFBTSx3QkFBd0IsR0FBa0M7SUFDOUQsRUFBRSxFQUFFLGdCQUFnQjtJQUNwQixJQUFJLEVBQUUsZUFBZTtJQUNyQixZQUFZLEVBQUUsaUJBQWlCO0lBQy9CLFVBQVUsRUFBRSxVQUFVO0lBQ3RCLGFBQWEsRUFBRSxRQUFRO0lBQ3ZCLFFBQVEsRUFBRTtRQUNSLElBQUksRUFBRSxNQUFNO1FBQ1osVUFBVSxFQUFFLGtDQUFrQztRQUM5QyxNQUFNLEVBQUUsMEJBQTBCO1FBQ2xDLEdBQUcsRUFBRSxNQUFNO1FBQ1gsR0FBRyxFQUFFLE1BQU07UUFDWCxJQUFJLEVBQUUsS0FBSztLQUNaO0NBQ0YsQ0FBQztBQUVGOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSwwQ0FBMEMsR0FBRyxlQUFlLENBQUM7SUFDeEUsVUFBVSxFQUFFO1FBQ1YsS0FBSyxFQUFFLGlCQUFpQjtRQUN4QixhQUFhLEVBQUUsc1NBQXNTO1FBQ3JULE1BQU0sRUFBRSxrRUFBa0U7S0FDM0U7SUFDRCxnQ0FBZ0MsRUFBRTtRQUNoQyxLQUFLLEVBQUUsdUNBQXVDO1FBQzlDLGFBQWEsRUFBRSx3YkFBd2I7UUFDdmMsTUFBTSxFQUFFLGtFQUFrRTtLQUMzRTtJQUNELGlCQUFpQixFQUFFO1FBQ2pCLEtBQUssRUFBRSx3QkFBd0I7UUFDL0IsYUFBYSxFQUFFLG9SQUFvUjtRQUNuUyxNQUFNLEVBQUUsa0VBQWtFO0tBQzNFO0NBQ0YsQ0FBQyxDQUFDIn0=
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex, } from '../private/canonicaljson.js';
|
|
2
|
+
import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
1
3
|
/**
|
|
2
4
|
* Compatibility bridge for the existing generic platform capability. New
|
|
3
5
|
* filesystem requests intentionally have no generic IPlatformBinding mapping.
|
|
@@ -5,4 +7,38 @@
|
|
|
5
7
|
export const storageKindPlatformCapabilityIds = {
|
|
6
8
|
objectStorage: 'objectstorage',
|
|
7
9
|
};
|
|
8
|
-
|
|
10
|
+
export class StorageContractError extends Error {
|
|
11
|
+
constructor(reasonArg) {
|
|
12
|
+
super(`storage contract error: ${reasonArg}`);
|
|
13
|
+
this.name = 'StorageContractError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const fail = (reasonArg) => {
|
|
17
|
+
throw new StorageContractError(reasonArg);
|
|
18
|
+
};
|
|
19
|
+
const { normalizeStorageRequest } = createStorageContractReaders(fail);
|
|
20
|
+
/**
|
|
21
|
+
* The exact bytes a storage request's digest is taken over: the request normalized to its contract
|
|
22
|
+
* shape, written as strict canonical JSON — keys in UTF-16 code-unit order, no whitespace, no
|
|
23
|
+
* absent member carried as `null`.
|
|
24
|
+
*
|
|
25
|
+
* Anything that is not a storage request is refused rather than hashed, because a digest of an
|
|
26
|
+
* unjudged body would name a request no adapter could fulfil.
|
|
27
|
+
*/
|
|
28
|
+
export const canonicalizeStorageRequest = (valueArg) => {
|
|
29
|
+
return canonicalizeStrictJson(normalizeStorageRequest(valueArg, 'storage request'), fail);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The storage request digest: bare lowercase hex SHA-256 over the canonical request, with no
|
|
33
|
+
* prefix and nothing but the request in the hashed value.
|
|
34
|
+
*
|
|
35
|
+
* A control plane issues this value on a binding and a consumer re-derives it from the request it
|
|
36
|
+
* declared; the two only agree because they call this one function. A second derivation elsewhere
|
|
37
|
+
* — a prefix, or a wrapper object around the request — is a different value for the same binding,
|
|
38
|
+
* and the consumer would keep rejecting a binding that is in fact its own.
|
|
39
|
+
*/
|
|
40
|
+
export const createStorageRequestSha256 = async (valueArg) => {
|
|
41
|
+
return createCanonicalJsonSha256Hex(canonicalizeStorageRequest(valueArg), fail);
|
|
42
|
+
};
|
|
43
|
+
export { storageRequestCanonicalDigestGoldenVectors, } from './storage.golden.js';
|
|
44
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3BsYXRmb3JtL3N0b3JhZ2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUNMLHNCQUFzQixFQUN0Qiw0QkFBNEIsR0FDN0IsTUFBTSw2QkFBNkIsQ0FBQztBQUNyQyxPQUFPLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQVE3RTs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxnQ0FBZ0MsR0FBRztJQUM5QyxhQUFhLEVBQUUsZUFBZTtDQUN0QixDQUFDO0FBaU9YLE1BQU0sT0FBTyxvQkFBcUIsU0FBUSxLQUFLO0lBQzdDLFlBQW1CLFNBQWlCO1FBQ2xDLEtBQUssQ0FBQywyQkFBMkIsU0FBUyxFQUFFLENBQUMsQ0FBQztRQUM5QyxJQUFJLENBQUMsSUFBSSxHQUFHLHNCQUFzQixDQUFDO0lBQ3JDLENBQUM7Q0FDRjtBQUVELE1BQU0sSUFBSSxHQUFHLENBQUMsU0FBaUIsRUFBUyxFQUFFO0lBQ3hDLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUM1QyxDQUFDLENBQUM7QUFFRixNQUFNLEVBQUUsdUJBQXVCLEVBQUUsR0FBRyw0QkFBNEIsQ0FBQyxJQUFJLENBQUMsQ0FBQztBQUV2RTs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQUcsQ0FBQyxRQUFpQixFQUFVLEVBQUU7SUFDdEUsT0FBTyxzQkFBc0IsQ0FDM0IsdUJBQXVCLENBQUMsUUFBUSxFQUFFLGlCQUFpQixDQUFDLEVBQ3BELElBQUksQ0FDTCxDQUFDO0FBQ0osQ0FBQyxDQUFDO0FBRUY7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBRyxLQUFLLEVBQzdDLFFBQWlCLEVBQ0EsRUFBRTtJQUNuQixPQUFPLDRCQUE0QixDQUFDLDBCQUEwQixDQUFDLFFBQVEsQ0FBQyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBQ2xGLENBQUMsQ0FBQztBQUVGLE9BQU8sRUFDTCwwQ0FBMEMsR0FFM0MsTUFBTSxxQkFBcUIsQ0FBQyJ9
|