@tinycloud/share-sdk 1.4.4-beta.1 → 1.5.0-beta.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 +9 -12
- package/dist/{addressed-publish-DorqWQJ8.d.ts → addressed-publish-D4IWNXuw.d.ts} +9 -29
- package/dist/{addressed-publish-BPFFqD5s.d.cts → addressed-publish-qp_bEVYu.d.cts} +9 -29
- package/dist/index.cjs +2 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -27
- package/dist/index.js.map +1 -1
- package/dist/share-root.d.cts +1 -1
- package/dist/share-root.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,25 +23,22 @@ import {
|
|
|
23
23
|
// 1. Plan, before anything is written. The network must be the owner's default network with the EIP-55 checksummed address.
|
|
24
24
|
const plan = planPathBoundShare({ shareId, encryptionNetwork: defaultEncryptionNetworkForOwner(ownerDid) });
|
|
25
25
|
|
|
26
|
-
// 2. Hold
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// network (deleted keys count, and the count is shared with `apps/` keys): each shared file uses one slot, so validate everything first
|
|
33
|
-
// (`planPathBoundShare` refuses a bad share id or network before any key exists). A failure after the key exists still costs the slot and there
|
|
34
|
-
// is no key delete: retry with a NEW shareId.
|
|
26
|
+
// 2. Hold decrypt under `<network>/share/` (`plan.registrationPermissions`, or `pathBoundSessionPermissions({ encryptionNetwork })` in the
|
|
27
|
+
// sign-in manifest, before any share exists): the node refuses to register the share unless the owner
|
|
28
|
+
// holds it; a bare-network decrypt never covers a share key. No capability is needed to encrypt: the share's key is deterministic and
|
|
29
|
+
// `keys.encrypt` wraps to its public key, which the node serves without authentication. The node records the key when the share policy
|
|
30
|
+
// is registered (step 5), in the same transaction. Each published share adds one key record, which counts toward the network's key limit
|
|
31
|
+
// (shared with `apps/` keys); `planPathBoundShare` refuses a bad share id or network before anything is encrypted.
|
|
35
32
|
// The share id is ONE grammar everywhere (plan, size bound, publisher): `[a-z0-9][a-z0-9_-]{15,63}` (16 to 64 characters; a lowercase `randomUUID()`,
|
|
36
33
|
// with or without its hyphens, is valid). Uppercase, `.`, a leading `_` or `-`, and anything longer than 64 characters are refused.
|
|
37
34
|
|
|
38
|
-
// 3.
|
|
35
|
+
// 3. Encrypt under the share's key (`keys` is a named-key client: `tcw.keys`, `node.keys` or `NamedKeyClient`).
|
|
39
36
|
const encrypted = await encryptAddressedShareContent({ plan, keys, plaintext, metadata: { contentType } });
|
|
40
37
|
|
|
41
38
|
// 4. Store EXACTLY encrypted.storedBytes (the canonical v2 envelope) at the share's path.
|
|
42
39
|
await put(path, encrypted.storedBytes);
|
|
43
40
|
|
|
44
|
-
// 5. Bind and publish.
|
|
41
|
+
// 5. Bind and publish. Registering the policy records the share's key.
|
|
45
42
|
const contentSource = bindPathBoundContentSource({
|
|
46
43
|
plan, envelope: encrypted.envelope, storedBytes: encrypted.storedBytes, kvResource: `${spaceId}/kv/${path}`, selector: "exact",
|
|
47
44
|
});
|
|
@@ -50,4 +47,4 @@ await publishAddressedShare({ /* shareId, target, resource, actions, ... */ cont
|
|
|
50
47
|
|
|
51
48
|
`publishAddressedShare` refuses a content source that is not bound to its own share's key (`AddressedContentSourceError`, match on `code`: `key-path-required`, `key-path-mismatch`, `digest-invalid`, `network-invalid`) before it signs, registers or writes anything; there is no network-wide fallback. Do not call `encryptToNetwork` with a `keyPath` for a share: it returns a v1 envelope wrapped to the network key, which neither the node nor the recipient opens. `assertAddressedShareFits` and `addressedShareBodyUpperBound` derive the key path from `shareId`. Opening an older share that has no key path is unchanged.
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
Hand-written content sources are not supported: the publisher checks the path, the digest's format and the network, but only `bindPathBoundContentSource` derives the raw-byte digest the recipient and the node compare.
|
|
@@ -9,11 +9,11 @@ import { S as ShareAuthorizationRequired, a as ShareAuthorizationMethod } from '
|
|
|
9
9
|
* opens only that one key, never the network. This module is the single place that derives the path, validates the
|
|
10
10
|
* network, serializes the stored envelope and computes the digests, so no consumer hand-writes any of them.
|
|
11
11
|
*
|
|
12
|
-
* Call sequence: `planPathBoundShare` (before anything is written) ->
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
12
|
+
* Call sequence: `planPathBoundShare` (before anything is written) -> `encryptAddressedShareContent` (or the caller's own
|
|
13
|
+
* `keys.encrypt`, which encrypts to the key's public key) -> upload `storedBytes` -> `bindPathBoundContentSource` ->
|
|
14
|
+
* `publishAddressedShare`. The node records the share's key when the policy is registered; nothing creates it beforehand.
|
|
15
15
|
*/
|
|
16
|
-
type AddressedContentSourceErrorCode = "share-id-invalid" | "network-invalid" | "network-not-checksummed" | "key-path-required" | "key-path-mismatch" | "digest-invalid" | "
|
|
16
|
+
type AddressedContentSourceErrorCode = "share-id-invalid" | "network-invalid" | "network-not-checksummed" | "key-path-required" | "key-path-mismatch" | "digest-invalid" | "envelope-invalid" | "stored-bytes-mismatch";
|
|
17
17
|
/** A path-bound source could not be planned, built or accepted. Match on `code`, not the message or `instanceof`. */
|
|
18
18
|
declare class AddressedContentSourceError extends TypeError {
|
|
19
19
|
readonly code: AddressedContentSourceErrorCode;
|
|
@@ -56,18 +56,7 @@ interface PathBoundSharePlan {
|
|
|
56
56
|
readonly keyPath: string;
|
|
57
57
|
/** `<network>/share/<shareId>`: the one decrypt resource the share grants. */
|
|
58
58
|
readonly keyResource: string;
|
|
59
|
-
/**
|
|
60
|
-
* ROUTE A, per share at runtime: `key.create` on exactly this share's key, requested with `requestPermissions` before the key is created
|
|
61
|
-
* (one extra consent per share). For apps that cannot use the sign-in route (for example apps whose sign-in goes through the OpenKey widget
|
|
62
|
-
* `authorizeTinyCloud` route, which refuses decrypt and key.create outside `apps/`).
|
|
63
|
-
*/
|
|
64
|
-
readonly requiredPermissions: PathBoundPermissionEntry[];
|
|
65
|
-
/**
|
|
66
|
-
* ROUTE B, once at sign-in (manifest entries): `key.create` and `decrypt` on the default network's `<network>/share/` prefix, so every share's
|
|
67
|
-
* key can be created without a further prompt. Put these in the `permissions` of the manifest the owner signs in with.
|
|
68
|
-
*/
|
|
69
|
-
readonly sessionPermissions: PathBoundPermissionEntry[];
|
|
70
|
-
/** The owner session must hold these for the node to register the policy: decrypt under `<network>/share/` (also part of `sessionPermissions`). */
|
|
59
|
+
/** The owner session must hold these for the node to register the policy: decrypt under `<network>/share/`. */
|
|
71
60
|
readonly registrationPermissions: PathBoundPermissionEntry[];
|
|
72
61
|
/** Options for `keys.encrypt(plan.keyPath, plaintext, plan.encryptOptions({ metadata }))`. */
|
|
73
62
|
encryptOptions(extra?: {
|
|
@@ -78,8 +67,8 @@ interface PathBoundSharePlan {
|
|
|
78
67
|
};
|
|
79
68
|
}
|
|
80
69
|
/**
|
|
81
|
-
* The manifest entries
|
|
82
|
-
*
|
|
70
|
+
* The manifest entries that let one session register every share's policy: `decrypt` on the default network's `<network>/share/` prefix.
|
|
71
|
+
* Needs no share id, so an app builds its sign-in manifest before any share exists. Put the result in `manifest.permissions`.
|
|
83
72
|
*/
|
|
84
73
|
declare function pathBoundSessionPermissions(input: {
|
|
85
74
|
readonly encryptionNetwork: string;
|
|
@@ -114,14 +103,6 @@ declare function bindPathBoundContentSource(input: {
|
|
|
114
103
|
}): PathBoundContentSource;
|
|
115
104
|
/** The part of a named-key client (`NamedKeyClient`, web `tcw.keys`, node `node.keys`) this module calls. */
|
|
116
105
|
interface AddressedShareKeyClient {
|
|
117
|
-
createKey(path: string, options: {
|
|
118
|
-
networkId: string;
|
|
119
|
-
}): Promise<{
|
|
120
|
-
readonly networkId: string;
|
|
121
|
-
readonly keyPath: string;
|
|
122
|
-
readonly state?: string;
|
|
123
|
-
readonly keyVersion?: number;
|
|
124
|
-
}>;
|
|
125
106
|
encrypt(path: string, plaintext: Uint8Array, options: {
|
|
126
107
|
networkId: string;
|
|
127
108
|
metadata?: Record<string, string>;
|
|
@@ -134,9 +115,8 @@ interface EncryptedAddressedContent {
|
|
|
134
115
|
readonly byteLength: number;
|
|
135
116
|
}
|
|
136
117
|
/**
|
|
137
|
-
*
|
|
138
|
-
* (
|
|
139
|
-
* `plan.requiredPermissions` must already be held; an unauthorized answer is reported as `key-not-authorized` naming it.
|
|
118
|
+
* Encrypt the content under the share's named key. `keys.encrypt` wraps to the key's public key, which the node serves without
|
|
119
|
+
* authentication; the key needs no record yet (the node records it when the share policy is registered).
|
|
140
120
|
*/
|
|
141
121
|
declare function encryptAddressedShareContent(input: {
|
|
142
122
|
readonly plan: PathBoundSharePlan;
|
|
@@ -9,11 +9,11 @@ import { S as ShareAuthorizationRequired, a as ShareAuthorizationMethod } from '
|
|
|
9
9
|
* opens only that one key, never the network. This module is the single place that derives the path, validates the
|
|
10
10
|
* network, serializes the stored envelope and computes the digests, so no consumer hand-writes any of them.
|
|
11
11
|
*
|
|
12
|
-
* Call sequence: `planPathBoundShare` (before anything is written) ->
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
12
|
+
* Call sequence: `planPathBoundShare` (before anything is written) -> `encryptAddressedShareContent` (or the caller's own
|
|
13
|
+
* `keys.encrypt`, which encrypts to the key's public key) -> upload `storedBytes` -> `bindPathBoundContentSource` ->
|
|
14
|
+
* `publishAddressedShare`. The node records the share's key when the policy is registered; nothing creates it beforehand.
|
|
15
15
|
*/
|
|
16
|
-
type AddressedContentSourceErrorCode = "share-id-invalid" | "network-invalid" | "network-not-checksummed" | "key-path-required" | "key-path-mismatch" | "digest-invalid" | "
|
|
16
|
+
type AddressedContentSourceErrorCode = "share-id-invalid" | "network-invalid" | "network-not-checksummed" | "key-path-required" | "key-path-mismatch" | "digest-invalid" | "envelope-invalid" | "stored-bytes-mismatch";
|
|
17
17
|
/** A path-bound source could not be planned, built or accepted. Match on `code`, not the message or `instanceof`. */
|
|
18
18
|
declare class AddressedContentSourceError extends TypeError {
|
|
19
19
|
readonly code: AddressedContentSourceErrorCode;
|
|
@@ -56,18 +56,7 @@ interface PathBoundSharePlan {
|
|
|
56
56
|
readonly keyPath: string;
|
|
57
57
|
/** `<network>/share/<shareId>`: the one decrypt resource the share grants. */
|
|
58
58
|
readonly keyResource: string;
|
|
59
|
-
/**
|
|
60
|
-
* ROUTE A, per share at runtime: `key.create` on exactly this share's key, requested with `requestPermissions` before the key is created
|
|
61
|
-
* (one extra consent per share). For apps that cannot use the sign-in route (for example apps whose sign-in goes through the OpenKey widget
|
|
62
|
-
* `authorizeTinyCloud` route, which refuses decrypt and key.create outside `apps/`).
|
|
63
|
-
*/
|
|
64
|
-
readonly requiredPermissions: PathBoundPermissionEntry[];
|
|
65
|
-
/**
|
|
66
|
-
* ROUTE B, once at sign-in (manifest entries): `key.create` and `decrypt` on the default network's `<network>/share/` prefix, so every share's
|
|
67
|
-
* key can be created without a further prompt. Put these in the `permissions` of the manifest the owner signs in with.
|
|
68
|
-
*/
|
|
69
|
-
readonly sessionPermissions: PathBoundPermissionEntry[];
|
|
70
|
-
/** The owner session must hold these for the node to register the policy: decrypt under `<network>/share/` (also part of `sessionPermissions`). */
|
|
59
|
+
/** The owner session must hold these for the node to register the policy: decrypt under `<network>/share/`. */
|
|
71
60
|
readonly registrationPermissions: PathBoundPermissionEntry[];
|
|
72
61
|
/** Options for `keys.encrypt(plan.keyPath, plaintext, plan.encryptOptions({ metadata }))`. */
|
|
73
62
|
encryptOptions(extra?: {
|
|
@@ -78,8 +67,8 @@ interface PathBoundSharePlan {
|
|
|
78
67
|
};
|
|
79
68
|
}
|
|
80
69
|
/**
|
|
81
|
-
* The manifest entries
|
|
82
|
-
*
|
|
70
|
+
* The manifest entries that let one session register every share's policy: `decrypt` on the default network's `<network>/share/` prefix.
|
|
71
|
+
* Needs no share id, so an app builds its sign-in manifest before any share exists. Put the result in `manifest.permissions`.
|
|
83
72
|
*/
|
|
84
73
|
declare function pathBoundSessionPermissions(input: {
|
|
85
74
|
readonly encryptionNetwork: string;
|
|
@@ -114,14 +103,6 @@ declare function bindPathBoundContentSource(input: {
|
|
|
114
103
|
}): PathBoundContentSource;
|
|
115
104
|
/** The part of a named-key client (`NamedKeyClient`, web `tcw.keys`, node `node.keys`) this module calls. */
|
|
116
105
|
interface AddressedShareKeyClient {
|
|
117
|
-
createKey(path: string, options: {
|
|
118
|
-
networkId: string;
|
|
119
|
-
}): Promise<{
|
|
120
|
-
readonly networkId: string;
|
|
121
|
-
readonly keyPath: string;
|
|
122
|
-
readonly state?: string;
|
|
123
|
-
readonly keyVersion?: number;
|
|
124
|
-
}>;
|
|
125
106
|
encrypt(path: string, plaintext: Uint8Array, options: {
|
|
126
107
|
networkId: string;
|
|
127
108
|
metadata?: Record<string, string>;
|
|
@@ -134,9 +115,8 @@ interface EncryptedAddressedContent {
|
|
|
134
115
|
readonly byteLength: number;
|
|
135
116
|
}
|
|
136
117
|
/**
|
|
137
|
-
*
|
|
138
|
-
* (
|
|
139
|
-
* `plan.requiredPermissions` must already be held; an unauthorized answer is reported as `key-not-authorized` naming it.
|
|
118
|
+
* Encrypt the content under the share's named key. `keys.encrypt` wraps to the key's public key, which the node serves without
|
|
119
|
+
* authentication; the key needs no record yet (the node records it when the share policy is registered).
|
|
140
120
|
*/
|
|
141
121
|
declare function encryptAddressedShareContent(input: {
|
|
142
122
|
readonly plan: PathBoundSharePlan;
|
package/dist/index.cjs
CHANGED
|
@@ -6633,7 +6633,7 @@ function canonicalKeyHashHex(encryptedSymmetricKey) {
|
|
|
6633
6633
|
}
|
|
6634
6634
|
function pathBoundSessionPermissions(input) {
|
|
6635
6635
|
const encryptionNetwork = assertNetwork(input.encryptionNetwork);
|
|
6636
|
-
return [{ service: "tinycloud.encryption", path: `${encryptionNetwork}/share/`, actions: ["tinycloud.encryption/decrypt"
|
|
6636
|
+
return [{ service: "tinycloud.encryption", path: `${encryptionNetwork}/share/`, actions: ["tinycloud.encryption/decrypt"] }];
|
|
6637
6637
|
}
|
|
6638
6638
|
function planPathBoundShare(input) {
|
|
6639
6639
|
const keyPath = pathBoundKeyPath(input.shareId);
|
|
@@ -6644,9 +6644,7 @@ function planPathBoundShare(input) {
|
|
|
6644
6644
|
encryptionNetwork,
|
|
6645
6645
|
keyPath,
|
|
6646
6646
|
keyResource,
|
|
6647
|
-
|
|
6648
|
-
sessionPermissions: pathBoundSessionPermissions({ encryptionNetwork }),
|
|
6649
|
-
registrationPermissions: [{ service: "tinycloud.encryption", path: `${encryptionNetwork}/share/`, actions: ["tinycloud.encryption/decrypt"] }],
|
|
6647
|
+
registrationPermissions: pathBoundSessionPermissions({ encryptionNetwork }),
|
|
6650
6648
|
encryptOptions: (extra) => ({ networkId: encryptionNetwork, ...extra?.metadata === void 0 ? {} : { metadata: { ...extra.metadata } } })
|
|
6651
6649
|
};
|
|
6652
6650
|
}
|
|
@@ -6700,34 +6698,11 @@ function bindPathBoundContentSource(input) {
|
|
|
6700
6698
|
initialCiphertextDigestHex: hex2((0, import_sha2562.sha256)(expected))
|
|
6701
6699
|
};
|
|
6702
6700
|
}
|
|
6703
|
-
function isUnauthorized(error) {
|
|
6704
|
-
const value = error;
|
|
6705
|
-
return value !== null && typeof value === "object" && (value.status === 401 || value.status === 403 || value.code === "PERMISSION_DENIED" || value.code === "AUTH_UNAUTHORIZED");
|
|
6706
|
-
}
|
|
6707
6701
|
async function encryptAddressedShareContent(input) {
|
|
6708
6702
|
const { plan, keys } = input;
|
|
6709
6703
|
const byteLength = input.plaintext.byteLength;
|
|
6710
|
-
let receipt;
|
|
6711
|
-
try {
|
|
6712
|
-
receipt = await keys.createKey(plan.keyPath, { networkId: plan.encryptionNetwork });
|
|
6713
|
-
} catch (error) {
|
|
6714
|
-
if (isUnauthorized(error)) {
|
|
6715
|
-
const status = error.status;
|
|
6716
|
-
throw new AddressedContentSourceError(
|
|
6717
|
-
"key-not-authorized",
|
|
6718
|
-
`the owner session does not hold tinycloud.encryption/key.create on ${plan.keyResource} (the node answers "Unauthorized Action: ${plan.keyResource} / tinycloud.encryption/key.create"); hold it either at sign-in (put plan.sessionPermissions in the manifest: key.create on ${plan.encryptionNetwork}/share/) or per share (requestPermissions(plan.requiredPermissions)) before creating the key` + (status === 403 ? "; if the owner signs in through OpenKey, the request must come from a first-party TinyCloud origin" : "")
|
|
6719
|
-
);
|
|
6720
|
-
}
|
|
6721
|
-
throw error;
|
|
6722
|
-
}
|
|
6723
|
-
if (receipt.networkId !== plan.encryptionNetwork || receipt.keyPath !== plan.keyPath || receipt.state !== void 0 && receipt.state !== "active") {
|
|
6724
|
-
throw new AddressedContentSourceError("key-receipt-mismatch", "the created key does not match this share");
|
|
6725
|
-
}
|
|
6726
6704
|
const envelope = await keys.encrypt(plan.keyPath, input.plaintext, plan.encryptOptions(input.metadata === void 0 ? void 0 : { metadata: input.metadata }));
|
|
6727
6705
|
assertEnvelope(plan, envelope);
|
|
6728
|
-
if (receipt.keyVersion !== void 0 && envelope.keyVersion !== receipt.keyVersion) {
|
|
6729
|
-
throw new AddressedContentSourceError("key-receipt-mismatch", "the envelope key version does not match the created key");
|
|
6730
|
-
}
|
|
6731
6706
|
const storedBytes = pathBoundStoredBytes(envelope);
|
|
6732
6707
|
return { envelope, storedBytes, byteLength };
|
|
6733
6708
|
}
|