@haverstack/core 0.7.0 → 0.9.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/README.md +30 -14
- package/dist/access.d.ts +20 -7
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +37 -16
- package/dist/access.js.map +1 -1
- package/dist/adapter-entry.d.ts +12 -0
- package/dist/adapter-entry.d.ts.map +1 -0
- package/dist/adapter-entry.js +11 -0
- package/dist/adapter-entry.js.map +1 -0
- package/dist/attachment-download.d.ts +75 -0
- package/dist/attachment-download.d.ts.map +1 -0
- package/dist/attachment-download.js +128 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/auth.d.ts +88 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +145 -0
- package/dist/auth.js.map +1 -0
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +20 -3
- package/dist/combine.js.map +1 -1
- package/dist/did-entry.d.ts +12 -0
- package/dist/did-entry.d.ts.map +1 -0
- package/dist/did-entry.js +16 -0
- package/dist/did-entry.js.map +1 -0
- package/dist/did.d.ts +51 -0
- package/dist/did.d.ts.map +1 -0
- package/dist/did.js +153 -0
- package/dist/did.js.map +1 -0
- package/dist/id.d.ts +18 -6
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +39 -10
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts +11 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +38 -0
- package/dist/merge.js.map +1 -0
- package/dist/schema.d.ts +31 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +134 -11
- package/dist/schema.js.map +1 -1
- package/dist/stack.d.ts +748 -152
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +1805 -328
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts +84 -23
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +260 -19
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +281 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +26 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +51 -1
- package/dist/validate.js.map +1 -1
- package/dist/wire-entry.d.ts +17 -0
- package/dist/wire-entry.d.ts.map +1 -0
- package/dist/wire-entry.js +24 -0
- package/dist/wire-entry.js.map +1 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -14,25 +14,36 @@ npm install @haverstack/core
|
|
|
14
14
|
|
|
15
15
|
You'll also need a storage adapter:
|
|
16
16
|
|
|
17
|
-
- [`@haverstack/adapter-
|
|
17
|
+
- [`@haverstack/adapter-local`](https://www.npmjs.com/package/@haverstack/adapter-local) — local storage (native SQLite + disk), single-app/embedded or server use
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
import { Stack } from '@haverstack/core';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
import { generateDidKeypair, exportDidPrivateKeyJwk } from '@haverstack/core/did';
|
|
24
|
+
import { LocalAdapter } from '@haverstack/adapter-local';
|
|
25
|
+
import { writeFile } from 'node:fs/promises';
|
|
26
|
+
|
|
27
|
+
const dbPath = './my-stack.db';
|
|
28
|
+
const keyPath = './my-stack.key.json'; // see "Key custody" below for where this really belongs
|
|
29
|
+
|
|
30
|
+
// First run: neither file exists yet, so this generates an identity
|
|
31
|
+
// keypair and persists the private key before initializing. Every run
|
|
32
|
+
// after that: the db exists, so this just opens it — the entityId
|
|
33
|
+
// function below is never called, so no throwaway keypair is minted.
|
|
34
|
+
const adapter = await LocalAdapter.openOrInitialize({
|
|
35
|
+
path: dbPath,
|
|
29
36
|
timezone: 'America/New_York',
|
|
37
|
+
entityId: async () => {
|
|
38
|
+
const { did, privateKey } = await generateDidKeypair();
|
|
39
|
+
await writeFile(keyPath, JSON.stringify(await exportDidPrivateKeyJwk(privateKey)));
|
|
40
|
+
return did;
|
|
41
|
+
},
|
|
30
42
|
});
|
|
31
43
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
const stack = await Stack.create(adapter);
|
|
44
|
+
// ownerProfile creates your own _entity profile record on first run —
|
|
45
|
+
// safe to keep passing on every open, it's a no-op once the record exists.
|
|
46
|
+
const stack = await Stack.create(adapter, { ownerProfile: { name: 'Jane Smith' } });
|
|
36
47
|
|
|
37
48
|
// Define a type
|
|
38
49
|
await stack.defineType('com.example.myapp/note@1', 'Note', {
|
|
@@ -59,7 +70,6 @@ const notes = await stack.query({
|
|
|
59
70
|
});
|
|
60
71
|
|
|
61
72
|
// Tear down when done
|
|
62
|
-
await stack.flush();
|
|
63
73
|
await stack.close();
|
|
64
74
|
```
|
|
65
75
|
|
|
@@ -72,7 +82,13 @@ The fundamental unit of data. Every record has:
|
|
|
72
82
|
- A **Crockford base-32 ID** — time-sortable, human-readable, URL-safe
|
|
73
83
|
- A **type** — defined by the app that created it
|
|
74
84
|
- **Content** — a JSON object validated against the type's schema
|
|
75
|
-
- Optional: `parentId`, `entityId`, `appId`, `permissions`, `associations`
|
|
85
|
+
- Optional: `parentId`, `entityId`, `appId`, `principalId`, `permissions`, `associations`
|
|
86
|
+
|
|
87
|
+
### Identity
|
|
88
|
+
|
|
89
|
+
`entityId` is a DID string (e.g. `did:key:z6Mk...`) — a keypair, not a name issued by any provider. `generateDidKeypair()` mints the mandatory floor method, `did:key`. `_entity` records are local profile cards _about_ a DID (`{ did, name, handle? }`), not the identity itself — the petname pattern. See [Identity](https://github.com/haverstack/core/blob/main/docs/spec/identity.md) in the spec.
|
|
90
|
+
|
|
91
|
+
**Key custody.** Nothing in `@haverstack/core` or any adapter stores the `privateKey` — only the public `did` travels with stack data. Persisting it (JWK via `exportDidPrivateKeyJwk()`/`importDidPrivateKeyJwk()`, or the `CryptoKey` itself in a browser's IndexedDB) is on you. Losing it doesn't break anything local — but you can never again authenticate as that identity to a server, since `did:key` identity _is_ the key. See [Key custody](https://github.com/haverstack/core#key-custody) in the main README for per-platform recipes.
|
|
76
92
|
|
|
77
93
|
### Types
|
|
78
94
|
|
|
@@ -90,7 +106,7 @@ Tags, attachments, and relationships are unified under a single model:
|
|
|
90
106
|
|
|
91
107
|
```ts
|
|
92
108
|
{ kind: 'tag', label: 'favourite' }
|
|
93
|
-
{ kind: 'attachment', label: 'avatar', fileId: '...'
|
|
109
|
+
{ kind: 'attachment', label: 'avatar', fileId: '...' }
|
|
94
110
|
{ kind: 'relationship', label: 'reply-to', recordId: '...' }
|
|
95
111
|
```
|
|
96
112
|
|
package/dist/access.d.ts
CHANGED
|
@@ -6,20 +6,33 @@
|
|
|
6
6
|
* storage backend. Used by ScopedStack (see stack.ts) to enforce access
|
|
7
7
|
* control; exported standalone for callers that want the raw predicate.
|
|
8
8
|
*/
|
|
9
|
-
import type { RecordId, StackRecord } from './types.js';
|
|
9
|
+
import type { Association, EntityId, RecordId, StackRecord } from './types.js';
|
|
10
10
|
export type AccessMode = 'read' | 'write';
|
|
11
|
+
/** An entity's standing within a `_group` Record's roster. `admin` implies `member` for ACL purposes. */
|
|
12
|
+
export type GroupRole = 'member' | 'admin';
|
|
11
13
|
/**
|
|
12
14
|
* Resolves a Record by ID. Used to walk a `_group` Record's associations for
|
|
13
15
|
* membership checks without requiring a full StackAdapter.
|
|
14
16
|
*/
|
|
15
17
|
export type RecordResolver = (id: RecordId) => Promise<StackRecord | null>;
|
|
16
18
|
/**
|
|
17
|
-
* Check whether an entity has read or write access to a Record
|
|
19
|
+
* Check whether an entity has read or write access to a Record: absent
|
|
20
|
+
* permissions = owner only; public = anyone reads; entity = direct match;
|
|
21
|
+
* group = walk the _group Record's roster associations. See
|
|
22
|
+
* docs/spec/access-control.md § Record-level permissions.
|
|
18
23
|
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* The entity here is the **subject** — record-level permissions are written
|
|
25
|
+
* about who data is for, never about the software that carried the request.
|
|
26
|
+
* A delegated app's own authority is a separate question, asked of the
|
|
27
|
+
* principal. See docs/spec/access-control.md § Delegation: principal and
|
|
28
|
+
* subject.
|
|
23
29
|
*/
|
|
24
|
-
export declare function checkAccess(record: StackRecord,
|
|
30
|
+
export declare function checkAccess(record: StackRecord, subjectEntityId: EntityId | null, ownerEntityId: EntityId | null, mode: AccessMode, resolveRecord: RecordResolver): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Determine an entity's role within a `_group` Record's roster from its
|
|
33
|
+
* relationship associations. `admin` short-circuits — it's strictly more
|
|
34
|
+
* privileged than `member`, so a matching admin association wins regardless
|
|
35
|
+
* of association order.
|
|
36
|
+
*/
|
|
37
|
+
export declare function groupRoleFromAssociations(associations: Association[] | undefined, entityId: EntityId): GroupRole | null;
|
|
25
38
|
//# sourceMappingURL=access.d.ts.map
|
package/dist/access.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1C,yGAAyG;AACzG,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;AAE3E;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,eAAe,EAAE,QAAQ,GAAG,IAAI,EAChC,aAAa,EAAE,QAAQ,GAAG,IAAI,EAC9B,IAAI,EAAE,UAAU,EAChB,aAAa,EAAE,cAAc,GAC5B,OAAO,CAAC,OAAO,CAAC,CA4BlB;AAYD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,WAAW,EAAE,GAAG,SAAS,EACvC,QAAQ,EAAE,QAAQ,GACjB,SAAS,GAAG,IAAI,CASlB"}
|
package/dist/access.js
CHANGED
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* control; exported standalone for callers that want the raw predicate.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* Check whether an entity has read or write access to a Record
|
|
10
|
+
* Check whether an entity has read or write access to a Record: absent
|
|
11
|
+
* permissions = owner only; public = anyone reads; entity = direct match;
|
|
12
|
+
* group = walk the _group Record's roster associations. See
|
|
13
|
+
* docs/spec/access-control.md § Record-level permissions.
|
|
11
14
|
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* The entity here is the **subject** — record-level permissions are written
|
|
16
|
+
* about who data is for, never about the software that carried the request.
|
|
17
|
+
* A delegated app's own authority is a separate question, asked of the
|
|
18
|
+
* principal. See docs/spec/access-control.md § Delegation: principal and
|
|
19
|
+
* subject.
|
|
16
20
|
*/
|
|
17
|
-
export async function checkAccess(record,
|
|
21
|
+
export async function checkAccess(record, subjectEntityId, ownerEntityId, mode, resolveRecord) {
|
|
18
22
|
// Owner always has full access.
|
|
19
|
-
if (
|
|
23
|
+
if (subjectEntityId && subjectEntityId === ownerEntityId)
|
|
20
24
|
return true;
|
|
21
25
|
const perms = record.permissions;
|
|
22
26
|
// No permissions = private.
|
|
@@ -25,15 +29,16 @@ export async function checkAccess(record, requesterEntityId, ownerEntityId, mode
|
|
|
25
29
|
for (const p of perms) {
|
|
26
30
|
if (p.access === 'public' && mode === 'read')
|
|
27
31
|
return true;
|
|
28
|
-
if (p.access === 'entity' && p.entityId ===
|
|
32
|
+
if (p.access === 'entity' && p.entityId === subjectEntityId) {
|
|
29
33
|
if (mode === 'read' && p.read)
|
|
30
34
|
return true;
|
|
31
35
|
if (mode === 'write' && p.write)
|
|
32
36
|
return true;
|
|
33
37
|
}
|
|
34
|
-
if (p.access === 'group' &&
|
|
35
|
-
const
|
|
36
|
-
|
|
38
|
+
if (p.access === 'group' && subjectEntityId) {
|
|
39
|
+
const role = await resolveGroupRole(p.groupId, subjectEntityId, resolveRecord);
|
|
40
|
+
const satisfiesRole = p.role === 'admin' ? role === 'admin' : role !== null;
|
|
41
|
+
if (satisfiesRole) {
|
|
37
42
|
if (mode === 'read' && p.read)
|
|
38
43
|
return true;
|
|
39
44
|
if (mode === 'write' && p.write)
|
|
@@ -43,12 +48,28 @@ export async function checkAccess(record, requesterEntityId, ownerEntityId, mode
|
|
|
43
48
|
}
|
|
44
49
|
return false;
|
|
45
50
|
}
|
|
46
|
-
async function
|
|
51
|
+
async function resolveGroupRole(groupRecordId, entityId, resolveRecord) {
|
|
47
52
|
const group = await resolveRecord(groupRecordId);
|
|
48
53
|
if (!group)
|
|
49
|
-
return
|
|
50
|
-
return (group.associations
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
return null;
|
|
55
|
+
return groupRoleFromAssociations(group.associations, entityId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Determine an entity's role within a `_group` Record's roster from its
|
|
59
|
+
* relationship associations. `admin` short-circuits — it's strictly more
|
|
60
|
+
* privileged than `member`, so a matching admin association wins regardless
|
|
61
|
+
* of association order.
|
|
62
|
+
*/
|
|
63
|
+
export function groupRoleFromAssociations(associations, entityId) {
|
|
64
|
+
let role = null;
|
|
65
|
+
for (const a of associations ?? []) {
|
|
66
|
+
if (a.kind === 'relationship' && a.recordId === entityId) {
|
|
67
|
+
if (a.label === 'admin')
|
|
68
|
+
return 'admin';
|
|
69
|
+
if (a.label === 'member')
|
|
70
|
+
role = 'member';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return role;
|
|
53
74
|
}
|
|
54
75
|
//# sourceMappingURL=access.js.map
|
package/dist/access.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAmB,EACnB,eAAgC,EAChC,aAA8B,EAC9B,IAAgB,EAChB,aAA6B;IAE7B,gCAAgC;IAChC,IAAI,eAAe,IAAI,eAAe,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IAEtE,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;IAEjC,4BAA4B;IAC5B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE/C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QAE1D,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,eAAe,EAAE,CAAC;YAC5D,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC3C,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;QAC/C,CAAC;QAED,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,IAAI,eAAe,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,CAAC,CAAC,OAAO,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC;YAC/E,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;YAC5E,IAAI,aAAa,EAAE,CAAC;gBAClB,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI;oBAAE,OAAO,IAAI,CAAC;gBAC3C,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,KAAK;oBAAE,OAAO,IAAI,CAAC;YAC/C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,aAAuB,EACvB,QAAkB,EAClB,aAA6B;IAE7B,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,yBAAyB,CAAC,KAAK,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,YAAuC,EACvC,QAAkB;IAElB,IAAI,IAAI,GAAqB,IAAI,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,YAAY,IAAI,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACzD,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;gBAAE,OAAO,OAAO,CAAC;YACxC,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ;gBAAE,IAAI,GAAG,QAAQ,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @haverstack/core/adapter
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The interfaces a storage adapter implements — record storage, blob
|
|
5
|
+
* storage, and the capabilities an adapter declares. Audience: adapter
|
|
6
|
+
* implementers (see the in-repo adapters), not app or plugin code, which
|
|
7
|
+
* reads `stack.features` instead of these directly.
|
|
8
|
+
*/
|
|
9
|
+
export type { StackRecordAdapter, StackBlobAdapter, AdapterCapabilities, BlobFileInfo, ExpectedVersionOptions, } from './types.js';
|
|
10
|
+
export { combineAdapters } from './combine.js';
|
|
11
|
+
export { assertQueryCapabilities } from './stack.js';
|
|
12
|
+
//# sourceMappingURL=adapter-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-entry.d.ts","sourceRoot":"","sources":["../src/adapter-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @haverstack/core/adapter
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The interfaces a storage adapter implements — record storage, blob
|
|
5
|
+
* storage, and the capabilities an adapter declares. Audience: adapter
|
|
6
|
+
* implementers (see the in-repo adapters), not app or plugin code, which
|
|
7
|
+
* reads `stack.features` instead of these directly.
|
|
8
|
+
*/
|
|
9
|
+
export { combineAdapters } from './combine.js';
|
|
10
|
+
export { assertQueryCapabilities } from './stack.js';
|
|
11
|
+
//# sourceMappingURL=adapter-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-entry.js","sourceRoot":"","sources":["../src/adapter-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack — Attachment Download Content-Type Resolution
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Shared, pure implementation of the `GET /attachments/:fileId` dangerous-
|
|
5
|
+
* type policy (docs/spec/wire-format.md § Download). Core has no HTTP
|
|
6
|
+
* server of its own — this module exists so `haverstack/server` and any
|
|
7
|
+
* other server implementation share one safe-list and one forcing
|
|
8
|
+
* computation, rather than each re-deriving it and risking drift on a
|
|
9
|
+
* security-relevant policy (the same "written once, not a per-server
|
|
10
|
+
* convention" principle applied elsewhere to adapter invariants).
|
|
11
|
+
*
|
|
12
|
+
* The policy is result-not-source: `resolveAttachmentDownloadContentType()`
|
|
13
|
+
* computes a single candidate type from whichever source wins (explicit
|
|
14
|
+
* param, then filename extension, then stored metadata, then
|
|
15
|
+
* application/octet-stream) and applies the safe-list to *that value* —
|
|
16
|
+
* never to the source that produced it, so no source can route a
|
|
17
|
+
* dangerous type past the check.
|
|
18
|
+
*/
|
|
19
|
+
/** application/octet-stream: the forced replacement for any unsafe type. */
|
|
20
|
+
export declare const FORCED_CONTENT_TYPE = "application/octet-stream";
|
|
21
|
+
export declare const NOSNIFF_HEADER_NAME = "X-Content-Type-Options";
|
|
22
|
+
export declare const NOSNIFF_HEADER_VALUE = "nosniff";
|
|
23
|
+
/**
|
|
24
|
+
* Whether a MIME type may be served as-is (Content-Type set to it, no
|
|
25
|
+
* forced download). Checked against the normalized base type — any
|
|
26
|
+
* parameters or casing on the input don't affect the result.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isSafeAttachmentContentType(mimeType: string): boolean;
|
|
29
|
+
/** Infers a candidate MIME type from a filename's extension. Returns undefined for no extension or an unrecognized one — the caller falls through to the next source. */
|
|
30
|
+
export declare function inferContentTypeFromFilename(filename: string): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Which `_attachment@1` record establishes a fileId's mimeType — the
|
|
33
|
+
* "first-recorded" rule of docs/spec/attachments.md, as a total order:
|
|
34
|
+
* earliest `createdAt`, ties broken by the lower `id`.
|
|
35
|
+
*
|
|
36
|
+
* The tiebreak is what makes it a rule rather than a coincidence of scan
|
|
37
|
+
* order. Rejecting a conflicting mimeType at write time is check-then-act
|
|
38
|
+
* with no storage-level uniqueness behind it, so two racing first uploads
|
|
39
|
+
* of the same bytes can both land on a concurrent server. Determinism
|
|
40
|
+
* doesn't depend on that never happening — it depends on every reader
|
|
41
|
+
* ordering the records the same way, which is why core's write-time
|
|
42
|
+
* conflict check and a server's serving choice both come through here.
|
|
43
|
+
*
|
|
44
|
+
* Generic over the record shape so a server can pass its own row type.
|
|
45
|
+
* Records for other fileIds must be filtered out by the caller.
|
|
46
|
+
*/
|
|
47
|
+
export declare function firstRecordedAttachment<T extends {
|
|
48
|
+
id: string;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
}>(records: readonly T[]): T | undefined;
|
|
51
|
+
export type AttachmentDownloadContentType = {
|
|
52
|
+
/** The Content-Type header value to serve. */
|
|
53
|
+
contentType: string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the dangerous-type policy overrode the candidate. When true,
|
|
56
|
+
* the server MUST also force `Content-Disposition: attachment` — Content-
|
|
57
|
+
* Type forcing alone doesn't stop a browser from sniffing the body back
|
|
58
|
+
* into the original type without both nosniff (always required, see
|
|
59
|
+
* NOSNIFF_HEADER_NAME/VALUE) and a non-inline disposition.
|
|
60
|
+
*/
|
|
61
|
+
forced: boolean;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the Content-Type served for `GET /attachments/:fileId`
|
|
65
|
+
* (docs/spec/wire-format.md § Download): `?contentType` wins, else
|
|
66
|
+
* extension inference from `?filename`, else the stored mimeType, else
|
|
67
|
+
* `application/octet-stream` — then the safe-list is applied to whichever
|
|
68
|
+
* candidate that produced.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveAttachmentDownloadContentType(input: {
|
|
71
|
+
contentTypeParam?: string;
|
|
72
|
+
filenameParam?: string;
|
|
73
|
+
storedMimeType?: string;
|
|
74
|
+
}): AttachmentDownloadContentType;
|
|
75
|
+
//# sourceMappingURL=attachment-download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.d.ts","sourceRoot":"","sources":["../src/attachment-download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAcH,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,6BAA6B,CAAC;AAE9D,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAO9C;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKrE;AAmCD,yKAAyK;AACzK,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,EAC/E,OAAO,EAAE,SAAS,CAAC,EAAE,GACpB,CAAC,GAAG,SAAS,CAOf;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAAC,KAAK,EAAE;IAC1D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,6BAA6B,CAUhC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack — Attachment Download Content-Type Resolution
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Shared, pure implementation of the `GET /attachments/:fileId` dangerous-
|
|
5
|
+
* type policy (docs/spec/wire-format.md § Download). Core has no HTTP
|
|
6
|
+
* server of its own — this module exists so `haverstack/server` and any
|
|
7
|
+
* other server implementation share one safe-list and one forcing
|
|
8
|
+
* computation, rather than each re-deriving it and risking drift on a
|
|
9
|
+
* security-relevant policy (the same "written once, not a per-server
|
|
10
|
+
* convention" principle applied elsewhere to adapter invariants).
|
|
11
|
+
*
|
|
12
|
+
* The policy is result-not-source: `resolveAttachmentDownloadContentType()`
|
|
13
|
+
* computes a single candidate type from whichever source wins (explicit
|
|
14
|
+
* param, then filename extension, then stored metadata, then
|
|
15
|
+
* application/octet-stream) and applies the safe-list to *that value* —
|
|
16
|
+
* never to the source that produced it, so no source can route a
|
|
17
|
+
* dangerous type past the check.
|
|
18
|
+
*/
|
|
19
|
+
/** MIME types that pass through unforced, regardless of source. */
|
|
20
|
+
const SAFE_EXACT_TYPES = new Set(['application/pdf', 'text/plain', 'application/octet-stream']);
|
|
21
|
+
/** MIME type prefixes that pass through unforced, except for UNSAFE_OVERRIDES below. */
|
|
22
|
+
const SAFE_PREFIXES = ['image/', 'video/', 'audio/'];
|
|
23
|
+
/**
|
|
24
|
+
* Matches a SAFE_PREFIXES entry but is script-capable and must still be
|
|
25
|
+
* forced — image/svg+xml is the reason a plain prefix check isn't enough.
|
|
26
|
+
*/
|
|
27
|
+
const UNSAFE_OVERRIDES = new Set(['image/svg+xml']);
|
|
28
|
+
/** application/octet-stream: the forced replacement for any unsafe type. */
|
|
29
|
+
export const FORCED_CONTENT_TYPE = 'application/octet-stream';
|
|
30
|
+
export const NOSNIFF_HEADER_NAME = 'X-Content-Type-Options';
|
|
31
|
+
export const NOSNIFF_HEADER_VALUE = 'nosniff';
|
|
32
|
+
/** Strips MIME parameters ("text/html; charset=utf-8" -> "text/html") and lowercases, so parameter tricks and casing can't bypass the safe-list check. */
|
|
33
|
+
function normalizeMimeType(mimeType) {
|
|
34
|
+
return mimeType.split(';')[0].trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether a MIME type may be served as-is (Content-Type set to it, no
|
|
38
|
+
* forced download). Checked against the normalized base type — any
|
|
39
|
+
* parameters or casing on the input don't affect the result.
|
|
40
|
+
*/
|
|
41
|
+
export function isSafeAttachmentContentType(mimeType) {
|
|
42
|
+
const normalized = normalizeMimeType(mimeType);
|
|
43
|
+
if (UNSAFE_OVERRIDES.has(normalized))
|
|
44
|
+
return false;
|
|
45
|
+
if (SAFE_EXACT_TYPES.has(normalized))
|
|
46
|
+
return true;
|
|
47
|
+
return SAFE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extension -> candidate MIME type, used only when resolving from a
|
|
51
|
+
* `?filename` param with no explicit `?contentType` (spec: extension
|
|
52
|
+
* inference). Deliberately includes dangerous types (html, svg, js, xml)
|
|
53
|
+
* — forcing applies to the computed candidate, not to how it was computed,
|
|
54
|
+
* so listing them here is what lets the safe-list catch them.
|
|
55
|
+
*/
|
|
56
|
+
const EXTENSION_MIME_TYPES = {
|
|
57
|
+
png: 'image/png',
|
|
58
|
+
jpg: 'image/jpeg',
|
|
59
|
+
jpeg: 'image/jpeg',
|
|
60
|
+
gif: 'image/gif',
|
|
61
|
+
webp: 'image/webp',
|
|
62
|
+
bmp: 'image/bmp',
|
|
63
|
+
avif: 'image/avif',
|
|
64
|
+
mp4: 'video/mp4',
|
|
65
|
+
webm: 'video/webm',
|
|
66
|
+
mov: 'video/quicktime',
|
|
67
|
+
mp3: 'audio/mpeg',
|
|
68
|
+
wav: 'audio/wav',
|
|
69
|
+
ogg: 'audio/ogg',
|
|
70
|
+
pdf: 'application/pdf',
|
|
71
|
+
txt: 'text/plain',
|
|
72
|
+
html: 'text/html',
|
|
73
|
+
htm: 'text/html',
|
|
74
|
+
xhtml: 'application/xhtml+xml',
|
|
75
|
+
svg: 'image/svg+xml',
|
|
76
|
+
js: 'text/javascript',
|
|
77
|
+
mjs: 'text/javascript',
|
|
78
|
+
xml: 'application/xml',
|
|
79
|
+
xsl: 'text/xsl',
|
|
80
|
+
};
|
|
81
|
+
/** Infers a candidate MIME type from a filename's extension. Returns undefined for no extension or an unrecognized one — the caller falls through to the next source. */
|
|
82
|
+
export function inferContentTypeFromFilename(filename) {
|
|
83
|
+
const match = /\.([a-z0-9]+)$/i.exec(filename);
|
|
84
|
+
return match ? EXTENSION_MIME_TYPES[match[1].toLowerCase()] : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Which `_attachment@1` record establishes a fileId's mimeType — the
|
|
88
|
+
* "first-recorded" rule of docs/spec/attachments.md, as a total order:
|
|
89
|
+
* earliest `createdAt`, ties broken by the lower `id`.
|
|
90
|
+
*
|
|
91
|
+
* The tiebreak is what makes it a rule rather than a coincidence of scan
|
|
92
|
+
* order. Rejecting a conflicting mimeType at write time is check-then-act
|
|
93
|
+
* with no storage-level uniqueness behind it, so two racing first uploads
|
|
94
|
+
* of the same bytes can both land on a concurrent server. Determinism
|
|
95
|
+
* doesn't depend on that never happening — it depends on every reader
|
|
96
|
+
* ordering the records the same way, which is why core's write-time
|
|
97
|
+
* conflict check and a server's serving choice both come through here.
|
|
98
|
+
*
|
|
99
|
+
* Generic over the record shape so a server can pass its own row type.
|
|
100
|
+
* Records for other fileIds must be filtered out by the caller.
|
|
101
|
+
*/
|
|
102
|
+
export function firstRecordedAttachment(records) {
|
|
103
|
+
return records.reduce((first, record) => {
|
|
104
|
+
if (!first)
|
|
105
|
+
return record;
|
|
106
|
+
const delta = record.createdAt.getTime() - first.createdAt.getTime();
|
|
107
|
+
if (delta !== 0)
|
|
108
|
+
return delta < 0 ? record : first;
|
|
109
|
+
return record.id < first.id ? record : first;
|
|
110
|
+
}, undefined);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the Content-Type served for `GET /attachments/:fileId`
|
|
114
|
+
* (docs/spec/wire-format.md § Download): `?contentType` wins, else
|
|
115
|
+
* extension inference from `?filename`, else the stored mimeType, else
|
|
116
|
+
* `application/octet-stream` — then the safe-list is applied to whichever
|
|
117
|
+
* candidate that produced.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveAttachmentDownloadContentType(input) {
|
|
120
|
+
const candidate = input.contentTypeParam ||
|
|
121
|
+
(input.filenameParam && inferContentTypeFromFilename(input.filenameParam)) ||
|
|
122
|
+
input.storedMimeType ||
|
|
123
|
+
FORCED_CONTENT_TYPE;
|
|
124
|
+
return isSafeAttachmentContentType(candidate)
|
|
125
|
+
? { contentType: candidate, forced: false }
|
|
126
|
+
: { contentType: FORCED_CONTENT_TYPE, forced: true };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=attachment-download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-download.js","sourceRoot":"","sources":["../src/attachment-download.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack — Authentication handshake primitives
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The challenge–response handshake proves a client holds the key behind a
|
|
5
|
+
* DID, so a server can issue it a bearer token without an out-of-band
|
|
6
|
+
* secret handoff. See docs/spec/wire-format.md § Authentication.
|
|
7
|
+
*
|
|
8
|
+
* What lives here is the half both sides must agree on byte for byte: the
|
|
9
|
+
* payload that gets signed. A client builds it to sign; a server builds it
|
|
10
|
+
* to verify. Two implementations of "the same string" would diverge on the
|
|
11
|
+
* first ambiguity, and the failure mode is a signature that verifies
|
|
12
|
+
* against a payload nobody meant — so there is one constructor, exported,
|
|
13
|
+
* the same way the attachment content-type policy is shared rather than
|
|
14
|
+
* re-derived.
|
|
15
|
+
*
|
|
16
|
+
* Core verifies signatures; it does not run a server. Nonce storage,
|
|
17
|
+
* single-use enforcement and token issuance are the server's.
|
|
18
|
+
*/
|
|
19
|
+
import { type DidKeypair } from './did.js';
|
|
20
|
+
/**
|
|
21
|
+
* Malformed input to a handshake primitive — a bad origin, DID, or nonce,
|
|
22
|
+
* caught locally before anything is signed or verified. Outside the
|
|
23
|
+
* StackError hierarchy, like InvalidDidError: no Stack operation has begun,
|
|
24
|
+
* so there is no wire code or status to carry (docs/spec/wire-format.md
|
|
25
|
+
* § The taxonomy root).
|
|
26
|
+
*/
|
|
27
|
+
export declare class InvalidAuthChallengeError extends Error {
|
|
28
|
+
constructor(message?: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Domain-separation tag. Version it rather than the payload's shape: a
|
|
32
|
+
* future signing primitive picks its own tag, so no signature made for one
|
|
33
|
+
* purpose can ever verify as another.
|
|
34
|
+
*/
|
|
35
|
+
export declare const AUTH_PAYLOAD_LABEL = "haverstack-auth-v1";
|
|
36
|
+
export type AuthChallenge = {
|
|
37
|
+
/** The server's own public origin, e.g. "https://stack.example.com". */
|
|
38
|
+
origin: string;
|
|
39
|
+
/** The DID being proven. */
|
|
40
|
+
did: string;
|
|
41
|
+
/** The nonce the server issued for this DID. */
|
|
42
|
+
nonce: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Reduce a URL to the origin form the payload uses: scheme, host and a
|
|
46
|
+
* non-default port. A client derives it from the URL it dialed; a server
|
|
47
|
+
* MUST derive it from its own configured public origin rather than from a
|
|
48
|
+
* request header, which a client controls.
|
|
49
|
+
*/
|
|
50
|
+
export declare const authOriginFromUrl: (url: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* Build the exact bytes a client signs and a server verifies.
|
|
53
|
+
*
|
|
54
|
+
* The origin is in the payload because the nonce alone doesn't scope a
|
|
55
|
+
* signature to a server: one that a client connects to can fetch a
|
|
56
|
+
* challenge from the client's real stack, pass that nonce along, and
|
|
57
|
+
* redeem the returned signature as a token. Signing the origin the client
|
|
58
|
+
* believes it is talking to makes such a signature verify nowhere else.
|
|
59
|
+
*/
|
|
60
|
+
export declare const buildAuthChallengePayload: (challenge: AuthChallenge) => Uint8Array;
|
|
61
|
+
/** Sign a challenge with the private key behind its DID. */
|
|
62
|
+
export declare const signAuthChallenge: (privateKey: CryptoKey, challenge: AuthChallenge) => Promise<Uint8Array>;
|
|
63
|
+
/**
|
|
64
|
+
* Verify a challenge signature. Answers only "this signature was made by
|
|
65
|
+
* the key behind this DID, for this origin and nonce" — whether the nonce
|
|
66
|
+
* was ever issued, is unexpired, and has not already been spent is the
|
|
67
|
+
* server's to track, and all three matter.
|
|
68
|
+
*/
|
|
69
|
+
export declare const verifyAuthChallenge: (challenge: AuthChallenge, signature: Uint8Array) => Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* What a client hands an adapter to authenticate: a DID and a signing
|
|
72
|
+
* callback, never a private key. Key custody stays with the app (see
|
|
73
|
+
* docs/spec/identity.md), so a caller is free to back this with a hardware
|
|
74
|
+
* key, an OS keychain, or a prompt.
|
|
75
|
+
*/
|
|
76
|
+
export type DidCredential = {
|
|
77
|
+
did: string;
|
|
78
|
+
sign: (payload: Uint8Array) => Promise<Uint8Array>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Build a credential from a keypair, for callers holding key material in
|
|
82
|
+
* process and wanting no ceremony. Anything else implements DidCredential
|
|
83
|
+
* directly.
|
|
84
|
+
*/
|
|
85
|
+
export declare const didCredentialFromKeypair: (keypair: DidKeypair) => DidCredential;
|
|
86
|
+
export declare const base64urlEncode: (bytes: Uint8Array) => string;
|
|
87
|
+
export declare const base64urlDecode: (encoded: string) => Uint8Array;
|
|
88
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAA+C,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAMxF;;;;;;GAMG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,SAAK;CAIzB;AAMD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AASvD,MAAM,MAAM,aAAa,GAAG;IAC1B,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,MAa/C,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,WAAW,aAAa,KAAG,UAapE,CAAC;AAMF,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,EACrB,WAAW,aAAa,KACvB,OAAO,CAAC,UAAU,CAAkE,CAAC;AAExF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,aAAa,EACxB,WAAW,UAAU,KACpB,OAAO,CAAC,OAAO,CASjB,CAAC;AAMF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CACpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,UAAU,KAAG,aAG7D,CAAC;AAMH,eAAO,MAAM,eAAe,GAAI,OAAO,UAAU,KAAG,MAInD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,UAYjD,CAAC"}
|