@tinycloud/mcp 0.8.3-beta.16 → 0.8.3-beta.17
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/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +565 -16
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +564 -12
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +564 -16
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +2 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +563 -12
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +564 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +563 -12
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/http.cjs
CHANGED
|
@@ -272,16 +272,85 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
// src/remote-store.ts
|
|
275
|
-
var
|
|
275
|
+
var import_node_crypto3 = require("crypto");
|
|
276
276
|
var import_promises = require("fs/promises");
|
|
277
277
|
var import_node_path = require("path");
|
|
278
278
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
279
279
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
280
|
+
var import_artifacts = require("@tinycloud/operations/artifacts");
|
|
281
|
+
var import_state = require("@tinycloud/operations/state");
|
|
280
282
|
var import_operations = require("@tinycloud/operations");
|
|
281
283
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
282
284
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
285
|
+
|
|
286
|
+
// src/native-auth.ts
|
|
287
|
+
var import_node_crypto2 = require("crypto");
|
|
288
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
289
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
290
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
291
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
292
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
293
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
294
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
295
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
296
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
297
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
298
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
299
|
+
if (supplied === null || configured === void 0) return false;
|
|
300
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
301
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
302
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
|
|
303
|
+
}
|
|
304
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
305
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
306
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
307
|
+
function canonicalOwnerDid(value) {
|
|
308
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
309
|
+
if (!match) return void 0;
|
|
310
|
+
const chainId = Number(match[1]);
|
|
311
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
312
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
313
|
+
}
|
|
314
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
315
|
+
if (permissions.length !== 2) return void 0;
|
|
316
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
317
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
318
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
319
|
+
const sql = sqlEntries[0];
|
|
320
|
+
const kv = kvEntries[0];
|
|
321
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
322
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
323
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
324
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
325
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
326
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
327
|
+
}
|
|
328
|
+
function ownerFromApplicationsSpace(space) {
|
|
329
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
330
|
+
if (!match) return void 0;
|
|
331
|
+
const chainId = Number(match[1]);
|
|
332
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
333
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
334
|
+
}
|
|
335
|
+
function actionsEqual(actual, expected) {
|
|
336
|
+
const actualActions = [...actual].sort();
|
|
337
|
+
const expectedActions = [...expected].sort();
|
|
338
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
339
|
+
}
|
|
340
|
+
function hasCaveats(permission) {
|
|
341
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/remote-store.ts
|
|
283
345
|
var PROFILE = "agent";
|
|
284
346
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
347
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
348
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
349
|
+
"DELEGATION_EXPIRED",
|
|
350
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
351
|
+
"DELEGATION_HOST_MISMATCH",
|
|
352
|
+
"DELEGATION_REJECTED"
|
|
353
|
+
]);
|
|
285
354
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
286
355
|
var BOOTSTRAP_PERMISSIONS = [
|
|
287
356
|
{
|
|
@@ -323,6 +392,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
323
392
|
this.detail = detail;
|
|
324
393
|
}
|
|
325
394
|
};
|
|
395
|
+
var NativeGrantValidationError = class extends Error {
|
|
396
|
+
code;
|
|
397
|
+
constructor(status) {
|
|
398
|
+
const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
|
|
399
|
+
super(code);
|
|
400
|
+
this.name = "NativeGrantValidationError";
|
|
401
|
+
this.code = code;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
326
404
|
var RemoteTenantStore = class {
|
|
327
405
|
#config;
|
|
328
406
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -335,9 +413,186 @@ var RemoteTenantStore = class {
|
|
|
335
413
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
336
414
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
337
415
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
416
|
+
#nativeIdentity;
|
|
417
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
338
418
|
constructor(config) {
|
|
339
419
|
this.#config = config;
|
|
340
420
|
}
|
|
421
|
+
async nativeServiceIdentity() {
|
|
422
|
+
const identity = await this.#loadNativeServiceKey();
|
|
423
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
424
|
+
}
|
|
425
|
+
nativeTenantStateRoot(ownerDid) {
|
|
426
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
427
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
428
|
+
const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
429
|
+
return (0, import_node_path.join)(this.#config.stateDir, "native-tenants", tenant);
|
|
430
|
+
}
|
|
431
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
432
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
433
|
+
throw new NativeGrantValidationError("rejected");
|
|
434
|
+
}
|
|
435
|
+
const identity = await this.#loadNativeServiceKey();
|
|
436
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
437
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
438
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
439
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
440
|
+
let previous = null;
|
|
441
|
+
try {
|
|
442
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
443
|
+
if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
444
|
+
previous = stored;
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
448
|
+
}
|
|
449
|
+
if (previous !== null) {
|
|
450
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
451
|
+
const nextExpiry = expiresAt.getTime();
|
|
452
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
453
|
+
throw new NativeGrantValidationError("rejected");
|
|
454
|
+
}
|
|
455
|
+
if (previous.delegationCid === activated.cid) {
|
|
456
|
+
return {
|
|
457
|
+
ownerDid,
|
|
458
|
+
serviceDid: normalizeDid(identity.did),
|
|
459
|
+
expiresAt: previous.expiresAt,
|
|
460
|
+
stateRoot
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const session = {
|
|
465
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
466
|
+
delegationCid: activated.cid,
|
|
467
|
+
spaceId,
|
|
468
|
+
jwk: identity.jwk,
|
|
469
|
+
verificationMethod: identity.did,
|
|
470
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
471
|
+
};
|
|
472
|
+
const grant = {
|
|
473
|
+
version: 1,
|
|
474
|
+
ownerDid,
|
|
475
|
+
serviceDid: normalizeDid(identity.did),
|
|
476
|
+
serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
|
|
477
|
+
delegationCid: activated.cid,
|
|
478
|
+
spaceId,
|
|
479
|
+
permissions,
|
|
480
|
+
expiresAt: expiresAt.toISOString()
|
|
481
|
+
};
|
|
482
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
483
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
484
|
+
try {
|
|
485
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
486
|
+
name: PROFILE,
|
|
487
|
+
host: this.#config.nodeHost,
|
|
488
|
+
chainId: activated.delegation.chainId,
|
|
489
|
+
spaceName: "applications",
|
|
490
|
+
did: identity.did,
|
|
491
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
+
profile: PROFILE,
|
|
493
|
+
operatorType: "agent",
|
|
494
|
+
posture: "delegate-session",
|
|
495
|
+
ownerDid,
|
|
496
|
+
sessionDid: identity.did,
|
|
497
|
+
spaceId,
|
|
498
|
+
tinycloudHost: this.#config.nodeHost
|
|
499
|
+
});
|
|
500
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
501
|
+
await secureWrite(sessionPath, session);
|
|
502
|
+
const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
|
|
503
|
+
profile: PROFILE,
|
|
504
|
+
posture: "delegate-session",
|
|
505
|
+
operatorType: "agent",
|
|
506
|
+
host: this.#config.nodeHost,
|
|
507
|
+
sessionDid: identity.did,
|
|
508
|
+
ownerDid,
|
|
509
|
+
spaceId,
|
|
510
|
+
missing: permissions,
|
|
511
|
+
granted: [],
|
|
512
|
+
replace: true
|
|
513
|
+
}));
|
|
514
|
+
if (request.status !== "created") {
|
|
515
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
516
|
+
}
|
|
517
|
+
const imported = await (0, import_operations.invokeOperation)(
|
|
518
|
+
"tinycloud.auth.import",
|
|
519
|
+
1,
|
|
520
|
+
{ profile: PROFILE, stateRoot },
|
|
521
|
+
{
|
|
522
|
+
kind: "tinycloud.auth.delegation",
|
|
523
|
+
version: 1,
|
|
524
|
+
requestId: request.request.requestId,
|
|
525
|
+
delegationCid: activated.cid,
|
|
526
|
+
delegation: activated.delegation
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
if (imported.status === "error") {
|
|
530
|
+
throw new NativeGrantValidationError(
|
|
531
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
535
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
536
|
+
}
|
|
537
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
try {
|
|
540
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
541
|
+
} catch {
|
|
542
|
+
await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
543
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async nativeGrantForOwner(ownerDid) {
|
|
551
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
552
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
553
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
554
|
+
let stored;
|
|
555
|
+
try {
|
|
556
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
557
|
+
} catch {
|
|
558
|
+
return { status: "rejected" };
|
|
559
|
+
}
|
|
560
|
+
if (stored === null) return { status: "required" };
|
|
561
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
|
|
562
|
+
return { status: "rejected" };
|
|
563
|
+
}
|
|
564
|
+
const expires = Date.parse(stored.expiresAt);
|
|
565
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
566
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
567
|
+
try {
|
|
568
|
+
const identity = await this.#loadNativeServiceKey();
|
|
569
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
570
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
571
|
+
if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
|
|
572
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error instanceof NativeGrantValidationError) {
|
|
575
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
576
|
+
}
|
|
577
|
+
return { status: "unavailable" };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async assertNativeGrantActive(ownerDid) {
|
|
581
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
582
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
583
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
584
|
+
let stored;
|
|
585
|
+
try {
|
|
586
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
587
|
+
} catch {
|
|
588
|
+
throw new NativeGrantValidationError("rejected");
|
|
589
|
+
}
|
|
590
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
591
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
592
|
+
throw new NativeGrantValidationError("rejected");
|
|
593
|
+
}
|
|
594
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
595
|
+
}
|
|
341
596
|
tenantStateRoot(subject) {
|
|
342
597
|
return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
343
598
|
}
|
|
@@ -864,7 +1119,7 @@ var RemoteTenantStore = class {
|
|
|
864
1119
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
865
1120
|
);
|
|
866
1121
|
if (reusable !== void 0) return reusable;
|
|
867
|
-
const nonce = (0,
|
|
1122
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
868
1123
|
const created = Date.now();
|
|
869
1124
|
const pending = {
|
|
870
1125
|
version: 1,
|
|
@@ -889,7 +1144,7 @@ var RemoteTenantStore = class {
|
|
|
889
1144
|
const expected = this.#signState(tenantId, nonce);
|
|
890
1145
|
const supplied = Buffer.from(signature, "base64url");
|
|
891
1146
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
892
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1147
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
893
1148
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
894
1149
|
}
|
|
895
1150
|
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -940,11 +1195,128 @@ var RemoteTenantStore = class {
|
|
|
940
1195
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
941
1196
|
return url.toString();
|
|
942
1197
|
}
|
|
1198
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1199
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1200
|
+
let releaseNext;
|
|
1201
|
+
const next = new Promise((resolve) => {
|
|
1202
|
+
releaseNext = resolve;
|
|
1203
|
+
});
|
|
1204
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1205
|
+
await previous;
|
|
1206
|
+
try {
|
|
1207
|
+
return await action();
|
|
1208
|
+
} finally {
|
|
1209
|
+
releaseNext();
|
|
1210
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async #loadNativeServiceKey() {
|
|
1214
|
+
if (this.#nativeIdentity === void 0) {
|
|
1215
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1216
|
+
this.#nativeIdentity = loading;
|
|
1217
|
+
void loading.catch(() => {
|
|
1218
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
return this.#nativeIdentity;
|
|
1222
|
+
}
|
|
1223
|
+
async #readOrCreateNativeServiceKey() {
|
|
1224
|
+
const path = (0, import_node_path.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1225
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1226
|
+
let stored = await readJson(path);
|
|
1227
|
+
if (stored === null) {
|
|
1228
|
+
const generated = generateSessionKey();
|
|
1229
|
+
try {
|
|
1230
|
+
await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1231
|
+
`, {
|
|
1232
|
+
mode: 384,
|
|
1233
|
+
flag: "wx"
|
|
1234
|
+
});
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1237
|
+
}
|
|
1238
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1239
|
+
stored = await readJson(path);
|
|
1240
|
+
}
|
|
1241
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1242
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1243
|
+
}
|
|
1244
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1245
|
+
}
|
|
1246
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1247
|
+
let delegation;
|
|
1248
|
+
try {
|
|
1249
|
+
const parsed = JSON.parse(serialized);
|
|
1250
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1251
|
+
delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
|
|
1252
|
+
} catch {
|
|
1253
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1254
|
+
}
|
|
1255
|
+
if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
|
|
1256
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1257
|
+
}
|
|
1258
|
+
const now = Date.now();
|
|
1259
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1260
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1261
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1262
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1263
|
+
}
|
|
1264
|
+
let ownerDid;
|
|
1265
|
+
let spaceId;
|
|
1266
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1267
|
+
try {
|
|
1268
|
+
await node.restoreSession({
|
|
1269
|
+
delegationHeader: delegation.delegationHeader,
|
|
1270
|
+
delegationCid: delegation.cid,
|
|
1271
|
+
spaceId: delegation.spaceId,
|
|
1272
|
+
jwk: identity.jwk,
|
|
1273
|
+
verificationMethod: identity.did,
|
|
1274
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1275
|
+
});
|
|
1276
|
+
const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
|
|
1277
|
+
host: this.#config.nodeHost,
|
|
1278
|
+
authorize: (permissions) => {
|
|
1279
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1280
|
+
const signedSpace = permissions[0]?.space;
|
|
1281
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1282
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1283
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1284
|
+
);
|
|
1285
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1286
|
+
ownerDid = derivedOwner;
|
|
1287
|
+
spaceId = signedSpace;
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1292
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
ownerDid,
|
|
1296
|
+
spaceId,
|
|
1297
|
+
permissions: [...activated.effectivePermissions],
|
|
1298
|
+
cid: activated.cid,
|
|
1299
|
+
expiresAt: activated.expiry,
|
|
1300
|
+
delegation: activated.delegation
|
|
1301
|
+
};
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1304
|
+
if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
|
|
1305
|
+
throw new NativeGrantValidationError(
|
|
1306
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
#nativeGrantPath(stateRoot) {
|
|
1313
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "native-grant.json");
|
|
1314
|
+
}
|
|
943
1315
|
#tenantId(subject) {
|
|
944
|
-
return (0,
|
|
1316
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
945
1317
|
}
|
|
946
1318
|
#signState(tenantId, nonce) {
|
|
947
|
-
return (0,
|
|
1319
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
948
1320
|
}
|
|
949
1321
|
#profilePath(stateRoot) {
|
|
950
1322
|
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -1033,6 +1405,21 @@ function samePrincipal(left, right) {
|
|
|
1033
1405
|
function normalizeDid(value) {
|
|
1034
1406
|
return value.split("#", 1)[0];
|
|
1035
1407
|
}
|
|
1408
|
+
function sameNodeHost(left, right) {
|
|
1409
|
+
try {
|
|
1410
|
+
const normalize = (value) => {
|
|
1411
|
+
const url = new URL(value);
|
|
1412
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
1413
|
+
url.hash = "";
|
|
1414
|
+
url.search = "";
|
|
1415
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1416
|
+
return url.toString().replace(/\/+$/, "");
|
|
1417
|
+
};
|
|
1418
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
1419
|
+
} catch {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1036
1423
|
function isPermissionEntry(value) {
|
|
1037
1424
|
return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
|
|
1038
1425
|
}
|
|
@@ -1046,7 +1433,7 @@ async function secureDirectory(path) {
|
|
|
1046
1433
|
async function secureWrite(path, value) {
|
|
1047
1434
|
const directory = (0, import_node_path.dirname)(path);
|
|
1048
1435
|
await secureDirectory(directory);
|
|
1049
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1436
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
1050
1437
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
1051
1438
|
`, { mode: 384 });
|
|
1052
1439
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1063,6 +1450,23 @@ async function readJson(path) {
|
|
|
1063
1450
|
throw error;
|
|
1064
1451
|
}
|
|
1065
1452
|
}
|
|
1453
|
+
async function readOptionalText(path) {
|
|
1454
|
+
try {
|
|
1455
|
+
return await (0, import_promises.readFile)(path, "utf8");
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async function restoreOptionalText(path, contents) {
|
|
1462
|
+
if (contents === null) {
|
|
1463
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1467
|
+
await (0, import_promises.writeFile)(path, contents, { mode: 384 });
|
|
1468
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1469
|
+
}
|
|
1066
1470
|
async function reusablePending(directory, expected) {
|
|
1067
1471
|
let names;
|
|
1068
1472
|
try {
|
|
@@ -1460,7 +1864,7 @@ function createJsonSchemaValidator() {
|
|
|
1460
1864
|
return new import_ajv.AjvJsonSchemaValidator();
|
|
1461
1865
|
}
|
|
1462
1866
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1463
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
1867
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
1464
1868
|
for (const binding of bindings) {
|
|
1465
1869
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1466
1870
|
if (operation === void 0) {
|
|
@@ -1538,7 +1942,7 @@ function outcomeCode(result) {
|
|
|
1538
1942
|
// package.json
|
|
1539
1943
|
var package_default = {
|
|
1540
1944
|
name: "@tinycloud/mcp",
|
|
1541
|
-
version: "0.8.3-beta.
|
|
1945
|
+
version: "0.8.3-beta.17",
|
|
1542
1946
|
description: "TinyCloud delegated operations MCP server",
|
|
1543
1947
|
keywords: [
|
|
1544
1948
|
"tinycloud",
|
|
@@ -1638,12 +2042,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1638
2042
|
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
1639
2043
|
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1640
2044
|
function createHostedMcpApp(config) {
|
|
2045
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2046
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2047
|
+
}
|
|
1641
2048
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1642
2049
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1643
2050
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1644
2051
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1645
2052
|
const plugin = createHostedSurface({
|
|
1646
2053
|
...config,
|
|
2054
|
+
nativeServiceCredential: void 0,
|
|
1647
2055
|
stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
|
|
1648
2056
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1649
2057
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1679,16 +2087,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1679
2087
|
resourceMetadataUrl
|
|
1680
2088
|
});
|
|
1681
2089
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2090
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2091
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1682
2092
|
const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1683
|
-
const
|
|
2093
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2094
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1684
2095
|
const { subject, ownerDids } = identity;
|
|
1685
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2096
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1686
2097
|
const validator = createJsonSchemaValidator();
|
|
1687
2098
|
const server = new import_server4.McpServer(
|
|
1688
2099
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1689
2100
|
{
|
|
1690
2101
|
jsonSchemaValidator: validator,
|
|
1691
|
-
instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
|
|
2102
|
+
instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
|
|
1692
2103
|
}
|
|
1693
2104
|
);
|
|
1694
2105
|
registerTinyCloudTools(server, {
|
|
@@ -1697,10 +2108,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1697
2108
|
explicitProfile: true,
|
|
1698
2109
|
allowOwnerProfile: false,
|
|
1699
2110
|
stateRoot,
|
|
1700
|
-
|
|
1701
|
-
|
|
2111
|
+
...nativeOwner === void 0 ? {} : {
|
|
2112
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2113
|
+
},
|
|
2114
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2115
|
+
...nativeOwner === void 0 ? {
|
|
2116
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2117
|
+
} : {}
|
|
1702
2118
|
}, validator);
|
|
1703
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2119
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1704
2120
|
return server;
|
|
1705
2121
|
}, {
|
|
1706
2122
|
legacy: "stateless",
|
|
@@ -1831,7 +2247,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1831
2247
|
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
1832
2248
|
}
|
|
1833
2249
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1834
|
-
|
|
2250
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2251
|
+
const card = {
|
|
1835
2252
|
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
1836
2253
|
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
1837
2254
|
title: "TinyCloud MCP",
|
|
@@ -1840,7 +2257,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1840
2257
|
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
1841
2258
|
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
1842
2259
|
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
1843
|
-
}
|
|
2260
|
+
};
|
|
2261
|
+
if (identity !== void 0) {
|
|
2262
|
+
card["x-tinycloud-native-auth"] = {
|
|
2263
|
+
serviceDid: identity.serviceDid,
|
|
2264
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2265
|
+
grantEndpoint: {
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
path: "/native/grant",
|
|
2268
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2269
|
+
},
|
|
2270
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2271
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2272
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2273
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2274
|
+
acceptedCapabilities: [
|
|
2275
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2276
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2277
|
+
],
|
|
2278
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2279
|
+
errors: {
|
|
2280
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2281
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2282
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2283
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2284
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2285
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return publicMetadataCors(Response.json(card, {
|
|
2290
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2291
|
+
}), request);
|
|
1844
2292
|
}
|
|
1845
2293
|
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
1846
2294
|
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
@@ -1938,6 +2386,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
|
|
|
1938
2386
|
);
|
|
1939
2387
|
}
|
|
1940
2388
|
}
|
|
2389
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2390
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2391
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2392
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2393
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2394
|
+
}
|
|
2395
|
+
if (!serviceCredentialMatches(
|
|
2396
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2397
|
+
config.nativeServiceCredential
|
|
2398
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2399
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2400
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2401
|
+
}
|
|
2402
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2403
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2404
|
+
let body;
|
|
2405
|
+
try {
|
|
2406
|
+
body = JSON.parse(bodyText);
|
|
2407
|
+
} catch {
|
|
2408
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2409
|
+
}
|
|
2410
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2411
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2412
|
+
}
|
|
2413
|
+
const grantBody = body;
|
|
2414
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2415
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2419
|
+
return json({
|
|
2420
|
+
ownerDid: accepted.ownerDid,
|
|
2421
|
+
serviceDid: accepted.serviceDid,
|
|
2422
|
+
expiresAt: accepted.expiresAt
|
|
2423
|
+
}, 201);
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
return nativeGrantErrorResponse(error);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2429
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2430
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2431
|
+
}
|
|
2432
|
+
if (!serviceCredentialMatches(
|
|
2433
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2434
|
+
config.nativeServiceCredential
|
|
2435
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2436
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2437
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2438
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2439
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2440
|
+
}
|
|
2441
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2442
|
+
if (grant.status !== "ok") {
|
|
2443
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2444
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2445
|
+
return json({ error: code }, status);
|
|
2446
|
+
}
|
|
2447
|
+
return handler.fetch(request, {
|
|
2448
|
+
authInfo: {
|
|
2449
|
+
token: "native-service",
|
|
2450
|
+
clientId: "tinycloud-native",
|
|
2451
|
+
scopes: [],
|
|
2452
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
1941
2456
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1942
2457
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1943
2458
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -2049,6 +2564,39 @@ function callbackCors(response, openkeyHost) {
|
|
|
2049
2564
|
headers.set("vary", "Origin");
|
|
2050
2565
|
return new Response(response.body, { status: response.status, headers });
|
|
2051
2566
|
}
|
|
2567
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2568
|
+
const contentLength = request.headers.get("content-length");
|
|
2569
|
+
if (contentLength !== null) {
|
|
2570
|
+
const parsedLength = Number(contentLength);
|
|
2571
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
if (request.body === null) return "";
|
|
2574
|
+
const reader = request.body.getReader();
|
|
2575
|
+
const chunks = [];
|
|
2576
|
+
let total = 0;
|
|
2577
|
+
try {
|
|
2578
|
+
while (true) {
|
|
2579
|
+
const { done, value } = await reader.read();
|
|
2580
|
+
if (done) break;
|
|
2581
|
+
total += value.byteLength;
|
|
2582
|
+
if (total > maxBytes) {
|
|
2583
|
+
await reader.cancel();
|
|
2584
|
+
return void 0;
|
|
2585
|
+
}
|
|
2586
|
+
chunks.push(value);
|
|
2587
|
+
}
|
|
2588
|
+
} finally {
|
|
2589
|
+
reader.releaseLock();
|
|
2590
|
+
}
|
|
2591
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2592
|
+
}
|
|
2593
|
+
function nativeGrantErrorResponse(error) {
|
|
2594
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2595
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2596
|
+
}
|
|
2597
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2598
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2599
|
+
}
|
|
2052
2600
|
function json(value, status = 200) {
|
|
2053
2601
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
2054
2602
|
}
|