@tinycloud/operations 0.5.0-beta.1 → 0.5.0-beta.3
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/coverage.json +10 -1
- package/dist/artifacts.d.cts +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/cli-runtime.cjs +728 -135
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.d.cts +1 -1
- package/dist/cli-runtime.d.ts +1 -1
- package/dist/cli-runtime.js +726 -134
- package/dist/cli-runtime.js.map +1 -1
- package/dist/{contract-DfyrluLB.d.cts → contract-zRklonbR.d.cts} +6 -3
- package/dist/{contract-DfyrluLB.d.ts → contract-zRklonbR.d.ts} +6 -3
- package/dist/delegation-binding.cjs +7 -0
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.d.cts +15 -3
- package/dist/delegation-binding.d.ts +15 -3
- package/dist/delegation-binding.js +6 -0
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +756 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +752 -133
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +5 -0
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.d.cts +14 -2
- package/dist/profile.d.ts +14 -2
- package/dist/profile.js +4 -0
- package/dist/profile.js.map +1 -1
- package/dist/recovery.cjs +67 -0
- package/dist/recovery.cjs.map +1 -0
- package/dist/recovery.d.cts +38 -0
- package/dist/recovery.d.ts +38 -0
- package/dist/recovery.js +39 -0
- package/dist/recovery.js.map +1 -0
- package/dist/secret-request.cjs +271 -0
- package/dist/secret-request.cjs.map +1 -0
- package/dist/secret-request.d.cts +119 -0
- package/dist/secret-request.d.ts +119 -0
- package/dist/secret-request.js +222 -0
- package/dist/secret-request.js.map +1 -0
- package/generated/operations.json +1838 -89
- package/package.json +15 -5
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/secret-request.ts
|
|
2
|
+
import * as nodeSdk from "@tinycloud/node-sdk";
|
|
3
|
+
var SECRET_MANAGER_ORIGIN = "https://secrets.tinycloud.xyz";
|
|
4
|
+
var SECRET_REQUEST_MAX_NAMES = 10;
|
|
5
|
+
var SECRET_REQUEST_MAX_URL_LENGTH = 8192;
|
|
6
|
+
var OWNER_RE = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40})$/;
|
|
7
|
+
var NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
8
|
+
var MAX_NAME_LENGTH = 64;
|
|
9
|
+
var SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10
|
+
var MAX_SCOPE_LENGTH = 64;
|
|
11
|
+
var RESERVED_SCOPES = ["default", "global", "scoped"];
|
|
12
|
+
var MAX_REQUESTER_LENGTH = 64;
|
|
13
|
+
var CONTROL_OR_FORMAT = /[\p{Cc}\p{Cf}]/u;
|
|
14
|
+
var STATE_RE = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
15
|
+
var MAX_RETURN_TO_LENGTH = 2048;
|
|
16
|
+
var RETURN_TO_ORIGINS = ["https://feed.tinycloud.xyz"];
|
|
17
|
+
var RETURN_TO_PREVIEW = /^https:\/\/[a-z0-9-]+\.tinyfeed\.pages\.dev$/;
|
|
18
|
+
var PKH_SPACE_RE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40}):[^:]+$/;
|
|
19
|
+
var SecretRequestInputError = class extends Error {
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "SecretRequestInputError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function canonicalSecretRequestOwner(did) {
|
|
27
|
+
const match = typeof did === "string" ? OWNER_RE.exec(did) : null;
|
|
28
|
+
if (match === null || !Number.isSafeInteger(Number(match[1]))) throw invalidOwner();
|
|
29
|
+
const address = match[2];
|
|
30
|
+
let checksummed;
|
|
31
|
+
try {
|
|
32
|
+
checksummed = nodeSdk.canonicalizeAddress(address);
|
|
33
|
+
} catch {
|
|
34
|
+
throw invalidOwner();
|
|
35
|
+
}
|
|
36
|
+
if (/[A-F]/.test(address.slice(2)) && address !== checksummed) throw invalidOwner();
|
|
37
|
+
return `did:pkh:eip155:${match[1]}:${checksummed}`;
|
|
38
|
+
}
|
|
39
|
+
function buildSecretRequestUrl(input) {
|
|
40
|
+
const owner = canonicalSecretRequestOwner(input.owner);
|
|
41
|
+
const names = validSecretNames(input.names);
|
|
42
|
+
if (input.scope !== void 0) assertSecretRequestScope(input.scope);
|
|
43
|
+
const requester = input.requester;
|
|
44
|
+
if (requester !== void 0 && (typeof requester !== "string" || requester.length === 0 || requester.length > MAX_REQUESTER_LENGTH || CONTROL_OR_FORMAT.test(requester))) {
|
|
45
|
+
throw new SecretRequestInputError("INVALID_REQUESTER", "The requester label must be 1-64 characters with no control or format characters.");
|
|
46
|
+
}
|
|
47
|
+
if (input.returnTo !== void 0 && !isAllowedReturnTo(input.returnTo)) {
|
|
48
|
+
throw new SecretRequestInputError("INVALID_RETURN_TO", "The return address is not an allowed Secret Manager callback.");
|
|
49
|
+
}
|
|
50
|
+
if (input.state !== void 0 && !STATE_RE.test(input.state)) {
|
|
51
|
+
throw new SecretRequestInputError("INVALID_STATE", "The state must be 1-128 characters of A-Z, a-z, 0-9, '.', '_', '~' or '-'.");
|
|
52
|
+
}
|
|
53
|
+
const parameters = [
|
|
54
|
+
["owner", owner],
|
|
55
|
+
...names.map((name) => ["name", name]),
|
|
56
|
+
...input.scope === void 0 ? [] : [["scope", input.scope]],
|
|
57
|
+
...input.requester === void 0 ? [] : [["requester", input.requester]],
|
|
58
|
+
...input.returnTo === void 0 ? [] : [["return_to", input.returnTo]],
|
|
59
|
+
...input.state === void 0 ? [] : [["state", input.state]],
|
|
60
|
+
["v", "1"]
|
|
61
|
+
];
|
|
62
|
+
const url = `${SECRET_MANAGER_ORIGIN}/request?${parameters.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&")}`;
|
|
63
|
+
if (url.length > SECRET_REQUEST_MAX_URL_LENGTH) {
|
|
64
|
+
throw new SecretRequestInputError("REQUEST_LINK_TOO_LONG", `The request link is longer than ${SECRET_REQUEST_MAX_URL_LENGTH} characters.`);
|
|
65
|
+
}
|
|
66
|
+
return { url, owner, names, ...input.scope === void 0 ? {} : { scope: input.scope } };
|
|
67
|
+
}
|
|
68
|
+
function secretSetupFor(link) {
|
|
69
|
+
return {
|
|
70
|
+
url: link.url,
|
|
71
|
+
names: [...link.names],
|
|
72
|
+
...link.scope === void 0 ? {} : { scope: link.scope },
|
|
73
|
+
owner: link.owner,
|
|
74
|
+
action: "add_secret"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function buildSecretManagerUrl(input = {}) {
|
|
78
|
+
const names = dedupe(input.names ?? []);
|
|
79
|
+
for (const name of names) assertSecretName(name);
|
|
80
|
+
if (input.scope !== void 0) assertSecretRequestScope(input.scope);
|
|
81
|
+
const parameters = [
|
|
82
|
+
...names.length === 1 ? [["secret", names[0]]] : [],
|
|
83
|
+
...names.length > 1 ? [["secrets", names.join(",")]] : [],
|
|
84
|
+
...input.scope === void 0 ? [] : [["scope", input.scope]]
|
|
85
|
+
];
|
|
86
|
+
const query = parameters.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
87
|
+
return `${SECRET_MANAGER_ORIGIN}/app${query === "" ? "" : `?${query}`}`;
|
|
88
|
+
}
|
|
89
|
+
function resolveSecretsOwner(profile, requestedOwner) {
|
|
90
|
+
const resolved = profile.local ? ownerOrUndefined(profile.principalDid) : ownerOrUndefined(profile.ownerDid) ?? (profile.posture === "delegate-session" ? ownerOfSpace(profile.space) : void 0) ?? ownerOrUndefined(profile.principalDid);
|
|
91
|
+
const requested = requestedOwner === void 0 ? void 0 : ownerOrUndefined(requestedOwner);
|
|
92
|
+
if (resolved !== void 0) {
|
|
93
|
+
if (requestedOwner !== void 0 && requested !== resolved) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
code: "OWNER_MISMATCH",
|
|
97
|
+
message: "The requested owner is not the owner of this identity's secrets."
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, owner: resolved, local: profile.local };
|
|
101
|
+
}
|
|
102
|
+
if (requested !== void 0 && !profile.local) return { ok: true, owner: requested, local: false };
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
code: "OWNER_DID_UNKNOWN",
|
|
106
|
+
message: "The owner of this identity's secrets is unknown. Pass the owner's did:pkh with --owner."
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function missingSecretRecovery(input) {
|
|
110
|
+
if (input.local) {
|
|
111
|
+
return { kind: "put_locally", hint: `Store it with: ${putCommands(input.names, input.scope)}` };
|
|
112
|
+
}
|
|
113
|
+
if (input.owner !== void 0) {
|
|
114
|
+
try {
|
|
115
|
+
return {
|
|
116
|
+
kind: "add_secret",
|
|
117
|
+
setup: secretSetupFor(buildSecretRequestUrl({
|
|
118
|
+
owner: input.owner,
|
|
119
|
+
names: input.names,
|
|
120
|
+
...input.scope === void 0 ? {} : { scope: input.scope }
|
|
121
|
+
}))
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: "not_requestable",
|
|
128
|
+
hint: `No Secret Manager request link can express this secret. The owner can store it with: ${putCommands(input.names, input.scope)}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function isSafeRequestAccessRecovery(value, deniedNames) {
|
|
132
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
133
|
+
const record = value;
|
|
134
|
+
const keys = Object.keys(record);
|
|
135
|
+
if (!keys.every((key) => key === "action" || key === "names" || key === "owner" || key === "scope")) return false;
|
|
136
|
+
if (record.action !== "request_access") return false;
|
|
137
|
+
const names = record.names;
|
|
138
|
+
if (!Array.isArray(names) || names.length === 0 || names.length > SECRET_REQUEST_MAX_NAMES || new Set(names).size !== names.length || !names.every((name) => typeof name === "string" && isValidSecretName(name) && deniedNames.includes(name))) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (typeof record.owner !== "string" || ownerOrUndefined(record.owner) !== record.owner) return false;
|
|
142
|
+
if ("scope" in record && (typeof record.scope !== "string" || !isValidSecretRequestScope(record.scope))) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function isValidSecretRequestScope(scope) {
|
|
146
|
+
return scope.length <= MAX_SCOPE_LENGTH && SCOPE_RE.test(scope) && !RESERVED_SCOPES.includes(scope);
|
|
147
|
+
}
|
|
148
|
+
function isValidSecretName(name) {
|
|
149
|
+
return name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
|
|
150
|
+
}
|
|
151
|
+
function validSecretNames(names) {
|
|
152
|
+
for (const name of names) assertSecretName(name);
|
|
153
|
+
const unique = dedupe(names);
|
|
154
|
+
if (unique.length === 0) {
|
|
155
|
+
throw new SecretRequestInputError("INVALID_SECRET_NAME", "At least one secret name is required.");
|
|
156
|
+
}
|
|
157
|
+
if (unique.length > SECRET_REQUEST_MAX_NAMES) {
|
|
158
|
+
throw new SecretRequestInputError("TOO_MANY_SECRET_NAMES", `At most ${SECRET_REQUEST_MAX_NAMES} secret names can be requested at once.`);
|
|
159
|
+
}
|
|
160
|
+
return unique;
|
|
161
|
+
}
|
|
162
|
+
function assertSecretName(name) {
|
|
163
|
+
if (typeof name !== "string" || !isValidSecretName(name)) {
|
|
164
|
+
throw new SecretRequestInputError("INVALID_SECRET_NAME", "Secret names are 1-64 characters of A-Z, 0-9 and '_', starting with a letter.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function assertSecretRequestScope(scope) {
|
|
168
|
+
if (typeof scope !== "string" || !isValidSecretRequestScope(scope)) {
|
|
169
|
+
throw new SecretRequestInputError("INVALID_SECRET_SCOPE", "The scope must be a canonical scope of at most 64 characters, not default, global or scoped.");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function isAllowedReturnTo(returnTo) {
|
|
173
|
+
if (typeof returnTo !== "string" || returnTo.length > MAX_RETURN_TO_LENGTH) return false;
|
|
174
|
+
let url;
|
|
175
|
+
try {
|
|
176
|
+
url = new URL(returnTo);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (url.username !== "" || url.password !== "") return false;
|
|
181
|
+
return RETURN_TO_ORIGINS.includes(url.origin) || RETURN_TO_PREVIEW.test(url.origin);
|
|
182
|
+
}
|
|
183
|
+
function ownerOrUndefined(did) {
|
|
184
|
+
if (did === void 0) return void 0;
|
|
185
|
+
try {
|
|
186
|
+
return canonicalSecretRequestOwner(did);
|
|
187
|
+
} catch {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function ownerOfSpace(space) {
|
|
192
|
+
const match = space === void 0 ? null : PKH_SPACE_RE.exec(space);
|
|
193
|
+
if (match === null) return void 0;
|
|
194
|
+
return ownerOrUndefined(`did:pkh:eip155:${match[1]}:${match[2].toLowerCase()}`);
|
|
195
|
+
}
|
|
196
|
+
function putCommands(names, scope) {
|
|
197
|
+
return dedupe(names).map((name) => `tc secrets put ${name}${scope === void 0 ? "" : ` --scope ${scope}`} --stdin`).join("; ");
|
|
198
|
+
}
|
|
199
|
+
function dedupe(names) {
|
|
200
|
+
return [...new Set(names)];
|
|
201
|
+
}
|
|
202
|
+
function invalidOwner() {
|
|
203
|
+
return new SecretRequestInputError("INVALID_OWNER", "The owner must be a did:pkh:eip155 account with a valid EIP-55 address.");
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
SECRET_MANAGER_ORIGIN,
|
|
207
|
+
SECRET_REQUEST_MAX_NAMES,
|
|
208
|
+
SECRET_REQUEST_MAX_URL_LENGTH,
|
|
209
|
+
SecretRequestInputError,
|
|
210
|
+
buildSecretManagerUrl,
|
|
211
|
+
buildSecretRequestUrl,
|
|
212
|
+
canonicalSecretRequestOwner,
|
|
213
|
+
isSafeRequestAccessRecovery,
|
|
214
|
+
isValidSecretName,
|
|
215
|
+
isValidSecretRequestScope,
|
|
216
|
+
missingSecretRecovery,
|
|
217
|
+
ownerOfSpace,
|
|
218
|
+
resolveSecretsOwner,
|
|
219
|
+
secretSetupFor,
|
|
220
|
+
validSecretNames
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=secret-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/secret-request.ts"],"sourcesContent":["// Namespace-shaped so modules that bundle this one stay compatible with\n// lightweight node-sdk test doubles.\nimport * as nodeSdk from \"@tinycloud/node-sdk\";\n\nimport type { TinyCloudPosture } from \"./contract.js\";\n\n/**\n * The one builder for Secret Manager links (TC-832 \"Link\"). Every\n * missing-secret path produces its `/request` link here, and `/app` links come\n * only from {@link buildSecretManagerUrl}. No parameter ever carries a value.\n */\nexport const SECRET_MANAGER_ORIGIN = \"https://secrets.tinycloud.xyz\";\nexport const SECRET_REQUEST_MAX_NAMES = 10;\nexport const SECRET_REQUEST_MAX_URL_LENGTH = 8192;\n\nconst OWNER_RE = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40})$/;\nconst NAME_RE = /^[A-Z][A-Z0-9_]*$/;\nconst MAX_NAME_LENGTH = 64;\nconst SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nconst MAX_SCOPE_LENGTH = 64;\nconst RESERVED_SCOPES: readonly string[] = [\"default\", \"global\", \"scoped\"];\nconst MAX_REQUESTER_LENGTH = 64;\nconst CONTROL_OR_FORMAT = /[\\p{Cc}\\p{Cf}]/u;\nconst STATE_RE = /^[A-Za-z0-9._~-]{1,128}$/;\nconst MAX_RETURN_TO_LENGTH = 2048;\nconst RETURN_TO_ORIGINS: readonly string[] = [\"https://feed.tinycloud.xyz\"];\nconst RETURN_TO_PREVIEW = /^https:\\/\\/[a-z0-9-]+\\.tinyfeed\\.pages\\.dev$/;\nconst PKH_SPACE_RE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40}):[^:]+$/;\n\nexport type SecretRequestInputErrorCode =\n | \"INVALID_SECRET_NAME\"\n | \"TOO_MANY_SECRET_NAMES\"\n | \"INVALID_SECRET_SCOPE\"\n | \"INVALID_OWNER\"\n | \"INVALID_REQUESTER\"\n | \"INVALID_RETURN_TO\"\n | \"INVALID_STATE\"\n | \"REQUEST_LINK_TOO_LONG\";\n\nexport class SecretRequestInputError extends Error {\n readonly code: SecretRequestInputErrorCode;\n\n constructor(code: SecretRequestInputErrorCode, message: string) {\n super(message);\n this.name = \"SecretRequestInputError\";\n this.code = code;\n }\n}\n\n/**\n * TC-832 owner rule: `did:pkh:eip155:<chain>:<address>` with a safe-integer\n * chain and a lowercase `0x`. An address with any uppercase letter must\n * already be its EIP-55 form. Returns the DID with the EIP-55 address.\n */\nexport function canonicalSecretRequestOwner(did: string): string {\n const match = typeof did === \"string\" ? OWNER_RE.exec(did) : null;\n if (match === null || !Number.isSafeInteger(Number(match[1]))) throw invalidOwner();\n const address = match[2]!;\n let checksummed: string;\n try {\n checksummed = nodeSdk.canonicalizeAddress(address);\n } catch {\n throw invalidOwner();\n }\n if (/[A-F]/.test(address.slice(2)) && address !== checksummed) throw invalidOwner();\n return `did:pkh:eip155:${match[1]}:${checksummed}`;\n}\n\nexport interface SecretRequestLinkInput {\n readonly owner: string;\n readonly names: readonly string[];\n readonly scope?: string;\n readonly requester?: string;\n readonly returnTo?: string;\n readonly state?: string;\n}\n\nexport interface SecretRequestLink {\n readonly url: string;\n readonly owner: string;\n readonly names: string[];\n readonly scope?: string;\n}\n\n/** Builds the canonical `/request` link, or throws {@link SecretRequestInputError}. */\nexport function buildSecretRequestUrl(input: SecretRequestLinkInput): SecretRequestLink {\n const owner = canonicalSecretRequestOwner(input.owner);\n const names = validSecretNames(input.names);\n if (input.scope !== undefined) assertSecretRequestScope(input.scope);\n const requester = input.requester;\n if (\n requester !== undefined &&\n (typeof requester !== \"string\" || requester.length === 0 || requester.length > MAX_REQUESTER_LENGTH ||\n CONTROL_OR_FORMAT.test(requester))\n ) {\n throw new SecretRequestInputError(\"INVALID_REQUESTER\", \"The requester label must be 1-64 characters with no control or format characters.\");\n }\n if (input.returnTo !== undefined && !isAllowedReturnTo(input.returnTo)) {\n throw new SecretRequestInputError(\"INVALID_RETURN_TO\", \"The return address is not an allowed Secret Manager callback.\");\n }\n if (input.state !== undefined && !STATE_RE.test(input.state)) {\n throw new SecretRequestInputError(\"INVALID_STATE\", \"The state must be 1-128 characters of A-Z, a-z, 0-9, '.', '_', '~' or '-'.\");\n }\n\n const parameters: [string, string][] = [\n [\"owner\", owner],\n ...names.map((name): [string, string] => [\"name\", name]),\n ...(input.scope === undefined ? [] : [[\"scope\", input.scope] as [string, string]]),\n ...(input.requester === undefined ? [] : [[\"requester\", input.requester] as [string, string]]),\n ...(input.returnTo === undefined ? [] : [[\"return_to\", input.returnTo] as [string, string]]),\n ...(input.state === undefined ? [] : [[\"state\", input.state] as [string, string]]),\n [\"v\", \"1\"],\n ];\n const url = `${SECRET_MANAGER_ORIGIN}/request?${parameters\n .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n .join(\"&\")}`;\n if (url.length > SECRET_REQUEST_MAX_URL_LENGTH) {\n throw new SecretRequestInputError(\"REQUEST_LINK_TOO_LONG\", `The request link is longer than ${SECRET_REQUEST_MAX_URL_LENGTH} characters.`);\n }\n return { url, owner, names, ...(input.scope === undefined ? {} : { scope: input.scope }) };\n}\n\n/** The value-free setup a missing secret reports: exactly these keys. */\nexport interface SecretSetup {\n readonly url: string;\n readonly names: string[];\n readonly scope?: string;\n readonly owner: string;\n readonly action: \"add_secret\";\n}\n\nexport function secretSetupFor(link: SecretRequestLink): SecretSetup {\n return {\n url: link.url,\n names: [...link.names],\n ...(link.scope === undefined ? {} : { scope: link.scope }),\n owner: link.owner,\n action: \"add_secret\",\n };\n}\n\n/**\n * The full manager (`/app`), the only path that replaces an existing value:\n * bare, `?secret=N`, or `?secrets=A,B`, plus `&scope=`. Names follow the link\n * name rule without the request cap.\n */\nexport function buildSecretManagerUrl(input: { readonly names?: readonly string[]; readonly scope?: string } = {}): string {\n const names = dedupe(input.names ?? []);\n for (const name of names) assertSecretName(name);\n if (input.scope !== undefined) assertSecretRequestScope(input.scope);\n const parameters: [string, string][] = [\n ...(names.length === 1 ? [[\"secret\", names[0]!] as [string, string]] : []),\n ...(names.length > 1 ? [[\"secrets\", names.join(\",\")] as [string, string]] : []),\n ...(input.scope === undefined ? [] : [[\"scope\", input.scope] as [string, string]]),\n ];\n const query = parameters.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join(\"&\");\n return `${SECRET_MANAGER_ORIGIN}/app${query === \"\" ? \"\" : `?${query}`}`;\n}\n\nexport type SecretsOwnerResolution =\n | { readonly ok: true; readonly owner: string; readonly local: boolean }\n | { readonly ok: false; readonly code: \"OWNER_DID_UNKNOWN\" | \"OWNER_MISMATCH\"; readonly message: string };\n\n/**\n * The owner whose secrets an identity reads, first match wins: a local key's\n * own account, the recorded owner, a delegate session's space owner, a pkh\n * principal, then the requested owner. A requested owner that disagrees with a\n * resolved one is `OWNER_MISMATCH`.\n */\nexport function resolveSecretsOwner(\n profile: {\n readonly local: boolean;\n readonly posture: TinyCloudPosture;\n readonly ownerDid?: string;\n readonly principalDid?: string;\n readonly space?: string;\n },\n requestedOwner?: string,\n): SecretsOwnerResolution {\n const resolved = profile.local\n ? ownerOrUndefined(profile.principalDid)\n : ownerOrUndefined(profile.ownerDid) ??\n (profile.posture === \"delegate-session\" ? ownerOfSpace(profile.space) : undefined) ??\n ownerOrUndefined(profile.principalDid);\n const requested = requestedOwner === undefined ? undefined : ownerOrUndefined(requestedOwner);\n\n if (resolved !== undefined) {\n if (requestedOwner !== undefined && requested !== resolved) {\n return {\n ok: false,\n code: \"OWNER_MISMATCH\",\n message: \"The requested owner is not the owner of this identity's secrets.\",\n };\n }\n return { ok: true, owner: resolved, local: profile.local };\n }\n if (requested !== undefined && !profile.local) return { ok: true, owner: requested, local: false };\n return {\n ok: false,\n code: \"OWNER_DID_UNKNOWN\",\n message: \"The owner of this identity's secrets is unknown. Pass the owner's did:pkh with --owner.\",\n };\n}\n\nexport type MissingSecretRecovery =\n | { readonly kind: \"add_secret\"; readonly setup: SecretSetup }\n | { readonly kind: \"put_locally\"; readonly hint: string }\n | { readonly kind: \"not_requestable\"; readonly hint: string };\n\n/**\n * How a caller gets missing secrets: a local identity stores them itself; any\n * other identity gets a `/request` link when one can express the request.\n * Never throws.\n */\nexport function missingSecretRecovery(input: {\n readonly owner: string | undefined;\n readonly local: boolean;\n readonly names: readonly string[];\n readonly scope?: string;\n}): MissingSecretRecovery {\n if (input.local) {\n return { kind: \"put_locally\", hint: `Store it with: ${putCommands(input.names, input.scope)}` };\n }\n if (input.owner !== undefined) {\n try {\n return {\n kind: \"add_secret\",\n setup: secretSetupFor(buildSecretRequestUrl({\n owner: input.owner,\n names: input.names,\n ...(input.scope === undefined ? {} : { scope: input.scope }),\n })),\n };\n } catch {\n // A legacy name or scope, or an unusable owner, cannot be requested.\n }\n }\n return {\n kind: \"not_requestable\",\n hint: `No Secret Manager request link can express this secret. The owner can store it with: ${putCommands(input.names, input.scope)}`,\n };\n}\n\n/**\n * The strict policy for a consent denial's `request_access` recovery: exact\n * keys, denied names only, a canonical owner and a valid scope. Any deviation\n * rejects the whole value.\n */\nexport function isSafeRequestAccessRecovery(\n value: unknown,\n deniedNames: readonly string[],\n): value is { action: \"request_access\"; names: string[]; owner: string; scope?: string } {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record);\n if (!keys.every((key) => key === \"action\" || key === \"names\" || key === \"owner\" || key === \"scope\")) return false;\n if (record.action !== \"request_access\") return false;\n const names = record.names;\n if (\n !Array.isArray(names) || names.length === 0 || names.length > SECRET_REQUEST_MAX_NAMES ||\n new Set(names).size !== names.length ||\n !names.every((name) => typeof name === \"string\" && isValidSecretName(name) && deniedNames.includes(name))\n ) {\n return false;\n }\n if (typeof record.owner !== \"string\" || ownerOrUndefined(record.owner) !== record.owner) return false;\n if (\"scope\" in record && (typeof record.scope !== \"string\" || !isValidSecretRequestScope(record.scope))) return false;\n return true;\n}\n\n/** TC-832 scope rule: canonical grammar, at most 64, not reserved. Never canonicalizes. */\nexport function isValidSecretRequestScope(scope: string): boolean {\n return scope.length <= MAX_SCOPE_LENGTH && SCOPE_RE.test(scope) && !RESERVED_SCOPES.includes(scope);\n}\n\n/** TC-832 name rule: `^[A-Z][A-Z0-9_]*$`, at most 64, no trimming. */\nexport function isValidSecretName(name: string): boolean {\n return name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);\n}\n\n/** Validated, de-duplicated (order kept) names, 1-10 of them. */\nexport function validSecretNames(names: readonly string[]): string[] {\n for (const name of names) assertSecretName(name);\n const unique = dedupe(names);\n if (unique.length === 0) {\n throw new SecretRequestInputError(\"INVALID_SECRET_NAME\", \"At least one secret name is required.\");\n }\n if (unique.length > SECRET_REQUEST_MAX_NAMES) {\n throw new SecretRequestInputError(\"TOO_MANY_SECRET_NAMES\", `At most ${SECRET_REQUEST_MAX_NAMES} secret names can be requested at once.`);\n }\n return unique;\n}\n\nfunction assertSecretName(name: string): void {\n if (typeof name !== \"string\" || !isValidSecretName(name)) {\n throw new SecretRequestInputError(\"INVALID_SECRET_NAME\", \"Secret names are 1-64 characters of A-Z, 0-9 and '_', starting with a letter.\");\n }\n}\n\nfunction assertSecretRequestScope(scope: string): void {\n if (typeof scope !== \"string\" || !isValidSecretRequestScope(scope)) {\n throw new SecretRequestInputError(\"INVALID_SECRET_SCOPE\", \"The scope must be a canonical scope of at most 64 characters, not default, global or scoped.\");\n }\n}\n\nfunction isAllowedReturnTo(returnTo: string): boolean {\n if (typeof returnTo !== \"string\" || returnTo.length > MAX_RETURN_TO_LENGTH) return false;\n let url: URL;\n try {\n url = new URL(returnTo);\n } catch {\n return false;\n }\n if (url.username !== \"\" || url.password !== \"\") return false;\n return RETURN_TO_ORIGINS.includes(url.origin) || RETURN_TO_PREVIEW.test(url.origin);\n}\n\nfunction ownerOrUndefined(did: string | undefined): string | undefined {\n if (did === undefined) return undefined;\n try {\n return canonicalSecretRequestOwner(did);\n } catch {\n return undefined;\n }\n}\n\n/** The canonical owner DID of a `tinycloud:pkh:eip155:…` space, or `undefined` for any other space. */\nexport function ownerOfSpace(space: string | undefined): string | undefined {\n const match = space === undefined ? null : PKH_SPACE_RE.exec(space);\n if (match === null) return undefined;\n // Space URIs carry the address in any case; the owner is its EIP-55 form.\n return ownerOrUndefined(`did:pkh:eip155:${match[1]}:${match[2]!.toLowerCase()}`);\n}\n\nfunction putCommands(names: readonly string[], scope: string | undefined): string {\n return dedupe(names)\n .map((name) => `tc secrets put ${name}${scope === undefined ? \"\" : ` --scope ${scope}`} --stdin`)\n .join(\"; \");\n}\n\nfunction dedupe(names: readonly string[]): string[] {\n return [...new Set(names)];\n}\n\nfunction invalidOwner(): SecretRequestInputError {\n return new SecretRequestInputError(\"INVALID_OWNER\", \"The owner must be a did:pkh:eip155 account with a valid EIP-55 address.\");\n}\n"],"mappings":";AAEA,YAAY,aAAa;AASlB,IAAM,wBAAwB;AAC9B,IAAM,2BAA2B;AACjC,IAAM,gCAAgC;AAE7C,IAAM,WAAW;AACjB,IAAM,UAAU;AAChB,IAAM,kBAAkB;AACxB,IAAM,WAAW;AACjB,IAAM,mBAAmB;AACzB,IAAM,kBAAqC,CAAC,WAAW,UAAU,QAAQ;AACzE,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAC1B,IAAM,WAAW;AACjB,IAAM,uBAAuB;AAC7B,IAAM,oBAAuC,CAAC,4BAA4B;AAC1E,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAYd,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAGjD,YAAY,MAAmC,SAAiB;AAC9D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,4BAA4B,KAAqB;AAC/D,QAAM,QAAQ,OAAO,QAAQ,WAAW,SAAS,KAAK,GAAG,IAAI;AAC7D,MAAI,UAAU,QAAQ,CAAC,OAAO,cAAc,OAAO,MAAM,CAAC,CAAC,CAAC,EAAG,OAAM,aAAa;AAClF,QAAM,UAAU,MAAM,CAAC;AACvB,MAAI;AACJ,MAAI;AACF,kBAAsB,4BAAoB,OAAO;AAAA,EACnD,QAAQ;AACN,UAAM,aAAa;AAAA,EACrB;AACA,MAAI,QAAQ,KAAK,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,YAAa,OAAM,aAAa;AAClF,SAAO,kBAAkB,MAAM,CAAC,CAAC,IAAI,WAAW;AAClD;AAmBO,SAAS,sBAAsB,OAAkD;AACtF,QAAM,QAAQ,4BAA4B,MAAM,KAAK;AACrD,QAAM,QAAQ,iBAAiB,MAAM,KAAK;AAC1C,MAAI,MAAM,UAAU,OAAW,0BAAyB,MAAM,KAAK;AACnE,QAAM,YAAY,MAAM;AACxB,MACE,cAAc,WACb,OAAO,cAAc,YAAY,UAAU,WAAW,KAAK,UAAU,SAAS,wBAC7E,kBAAkB,KAAK,SAAS,IAClC;AACA,UAAM,IAAI,wBAAwB,qBAAqB,mFAAmF;AAAA,EAC5I;AACA,MAAI,MAAM,aAAa,UAAa,CAAC,kBAAkB,MAAM,QAAQ,GAAG;AACtE,UAAM,IAAI,wBAAwB,qBAAqB,+DAA+D;AAAA,EACxH;AACA,MAAI,MAAM,UAAU,UAAa,CAAC,SAAS,KAAK,MAAM,KAAK,GAAG;AAC5D,UAAM,IAAI,wBAAwB,iBAAiB,4EAA4E;AAAA,EACjI;AAEA,QAAM,aAAiC;AAAA,IACrC,CAAC,SAAS,KAAK;AAAA,IACf,GAAG,MAAM,IAAI,CAAC,SAA2B,CAAC,QAAQ,IAAI,CAAC;AAAA,IACvD,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,KAAK,CAAqB;AAAA,IAChF,GAAI,MAAM,cAAc,SAAY,CAAC,IAAI,CAAC,CAAC,aAAa,MAAM,SAAS,CAAqB;AAAA,IAC5F,GAAI,MAAM,aAAa,SAAY,CAAC,IAAI,CAAC,CAAC,aAAa,MAAM,QAAQ,CAAqB;AAAA,IAC1F,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,KAAK,CAAqB;AAAA,IAChF,CAAC,KAAK,GAAG;AAAA,EACX;AACA,QAAM,MAAM,GAAG,qBAAqB,YAAY,WAC7C,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,mBAAmB,KAAK,CAAC,EAAE,EAC3D,KAAK,GAAG,CAAC;AACZ,MAAI,IAAI,SAAS,+BAA+B;AAC9C,UAAM,IAAI,wBAAwB,yBAAyB,mCAAmC,6BAA6B,cAAc;AAAA,EAC3I;AACA,SAAO,EAAE,KAAK,OAAO,OAAO,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM,EAAG;AAC3F;AAWO,SAAS,eAAe,MAAsC;AACnE,SAAO;AAAA,IACL,KAAK,KAAK;AAAA,IACV,OAAO,CAAC,GAAG,KAAK,KAAK;AAAA,IACrB,GAAI,KAAK,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM;AAAA,IACxD,OAAO,KAAK;AAAA,IACZ,QAAQ;AAAA,EACV;AACF;AAOO,SAAS,sBAAsB,QAAyE,CAAC,GAAW;AACzH,QAAM,QAAQ,OAAO,MAAM,SAAS,CAAC,CAAC;AACtC,aAAW,QAAQ,MAAO,kBAAiB,IAAI;AAC/C,MAAI,MAAM,UAAU,OAAW,0BAAyB,MAAM,KAAK;AACnE,QAAM,aAAiC;AAAA,IACrC,GAAI,MAAM,WAAW,IAAI,CAAC,CAAC,UAAU,MAAM,CAAC,CAAE,CAAqB,IAAI,CAAC;AAAA,IACxE,GAAI,MAAM,SAAS,IAAI,CAAC,CAAC,WAAW,MAAM,KAAK,GAAG,CAAC,CAAqB,IAAI,CAAC;AAAA,IAC7E,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,KAAK,CAAqB;AAAA,EAClF;AACA,QAAM,QAAQ,WAAW,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,mBAAmB,KAAK,CAAC,EAAE,EAAE,KAAK,GAAG;AAC9F,SAAO,GAAG,qBAAqB,OAAO,UAAU,KAAK,KAAK,IAAI,KAAK,EAAE;AACvE;AAYO,SAAS,oBACd,SAOA,gBACwB;AACxB,QAAM,WAAW,QAAQ,QACrB,iBAAiB,QAAQ,YAAY,IACrC,iBAAiB,QAAQ,QAAQ,MAChC,QAAQ,YAAY,qBAAqB,aAAa,QAAQ,KAAK,IAAI,WACxE,iBAAiB,QAAQ,YAAY;AACzC,QAAM,YAAY,mBAAmB,SAAY,SAAY,iBAAiB,cAAc;AAE5F,MAAI,aAAa,QAAW;AAC1B,QAAI,mBAAmB,UAAa,cAAc,UAAU;AAC1D,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,UAAU,OAAO,QAAQ,MAAM;AAAA,EAC3D;AACA,MAAI,cAAc,UAAa,CAAC,QAAQ,MAAO,QAAO,EAAE,IAAI,MAAM,OAAO,WAAW,OAAO,MAAM;AACjG,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AACF;AAYO,SAAS,sBAAsB,OAKZ;AACxB,MAAI,MAAM,OAAO;AACf,WAAO,EAAE,MAAM,eAAe,MAAM,kBAAkB,YAAY,MAAM,OAAO,MAAM,KAAK,CAAC,GAAG;AAAA,EAChG;AACA,MAAI,MAAM,UAAU,QAAW;AAC7B,QAAI;AACF,aAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO,eAAe,sBAAsB;AAAA,UAC1C,OAAO,MAAM;AAAA,UACb,OAAO,MAAM;AAAA,UACb,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM;AAAA,QAC5D,CAAC,CAAC;AAAA,MACJ;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,wFAAwF,YAAY,MAAM,OAAO,MAAM,KAAK,CAAC;AAAA,EACrI;AACF;AAOO,SAAS,4BACd,OACA,aACuF;AACvF,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,EAAG,QAAO;AAChF,QAAM,SAAS;AACf,QAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,MAAI,CAAC,KAAK,MAAM,CAAC,QAAQ,QAAQ,YAAY,QAAQ,WAAW,QAAQ,WAAW,QAAQ,OAAO,EAAG,QAAO;AAC5G,MAAI,OAAO,WAAW,iBAAkB,QAAO;AAC/C,QAAM,QAAQ,OAAO;AACrB,MACE,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,KAAK,MAAM,SAAS,4BAC9D,IAAI,IAAI,KAAK,EAAE,SAAS,MAAM,UAC9B,CAAC,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,YAAY,kBAAkB,IAAI,KAAK,YAAY,SAAS,IAAI,CAAC,GACxG;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,OAAO,UAAU,YAAY,iBAAiB,OAAO,KAAK,MAAM,OAAO,MAAO,QAAO;AAChG,MAAI,WAAW,WAAW,OAAO,OAAO,UAAU,YAAY,CAAC,0BAA0B,OAAO,KAAK,GAAI,QAAO;AAChH,SAAO;AACT;AAGO,SAAS,0BAA0B,OAAwB;AAChE,SAAO,MAAM,UAAU,oBAAoB,SAAS,KAAK,KAAK,KAAK,CAAC,gBAAgB,SAAS,KAAK;AACpG;AAGO,SAAS,kBAAkB,MAAuB;AACvD,SAAO,KAAK,UAAU,mBAAmB,QAAQ,KAAK,IAAI;AAC5D;AAGO,SAAS,iBAAiB,OAAoC;AACnE,aAAW,QAAQ,MAAO,kBAAiB,IAAI;AAC/C,QAAM,SAAS,OAAO,KAAK;AAC3B,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,wBAAwB,uBAAuB,uCAAuC;AAAA,EAClG;AACA,MAAI,OAAO,SAAS,0BAA0B;AAC5C,UAAM,IAAI,wBAAwB,yBAAyB,WAAW,wBAAwB,yCAAyC;AAAA,EACzI;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAoB;AAC5C,MAAI,OAAO,SAAS,YAAY,CAAC,kBAAkB,IAAI,GAAG;AACxD,UAAM,IAAI,wBAAwB,uBAAuB,+EAA+E;AAAA,EAC1I;AACF;AAEA,SAAS,yBAAyB,OAAqB;AACrD,MAAI,OAAO,UAAU,YAAY,CAAC,0BAA0B,KAAK,GAAG;AAClE,UAAM,IAAI,wBAAwB,wBAAwB,8FAA8F;AAAA,EAC1J;AACF;AAEA,SAAS,kBAAkB,UAA2B;AACpD,MAAI,OAAO,aAAa,YAAY,SAAS,SAAS,qBAAsB,QAAO;AACnF,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,QAAQ;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACvD,SAAO,kBAAkB,SAAS,IAAI,MAAM,KAAK,kBAAkB,KAAK,IAAI,MAAM;AACpF;AAEA,SAAS,iBAAiB,KAA6C;AACrE,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI;AACF,WAAO,4BAA4B,GAAG;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,aAAa,OAA+C;AAC1E,QAAM,QAAQ,UAAU,SAAY,OAAO,aAAa,KAAK,KAAK;AAClE,MAAI,UAAU,KAAM,QAAO;AAE3B,SAAO,iBAAiB,kBAAkB,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,EAAG,YAAY,CAAC,EAAE;AACjF;AAEA,SAAS,YAAY,OAA0B,OAAmC;AAChF,SAAO,OAAO,KAAK,EAChB,IAAI,CAAC,SAAS,kBAAkB,IAAI,GAAG,UAAU,SAAY,KAAK,YAAY,KAAK,EAAE,UAAU,EAC/F,KAAK,IAAI;AACd;AAEA,SAAS,OAAO,OAAoC;AAClD,SAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAC3B;AAEA,SAAS,eAAwC;AAC/C,SAAO,IAAI,wBAAwB,iBAAiB,yEAAyE;AAC/H;","names":[]}
|