@alessandroraffa/tangyr 0.14.1 → 0.16.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/dist/index.js +298 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15090,6 +15090,13 @@ function normalizeSource(declared) {
|
|
|
15090
15090
|
"Configuration declares source.type: remote but no source.url. Set source.url to the https endpoint of your kit origin."
|
|
15091
15091
|
);
|
|
15092
15092
|
}
|
|
15093
|
+
if (source.channel !== void 0) {
|
|
15094
|
+
if (!/^[a-z0-9][a-z0-9-]{0,31}$/u.test(source.channel)) {
|
|
15095
|
+
throw new Error(
|
|
15096
|
+
`Configuration declares source.channel "${source.channel}", which must be lowercase alphanumeric with hyphens, 1-32 characters.`
|
|
15097
|
+
);
|
|
15098
|
+
}
|
|
15099
|
+
}
|
|
15093
15100
|
return source;
|
|
15094
15101
|
}
|
|
15095
15102
|
function findConfigPath(explicitPath) {
|
|
@@ -17577,6 +17584,24 @@ import path15 from "path";
|
|
|
17577
17584
|
|
|
17578
17585
|
// src/core/kit-reference.ts
|
|
17579
17586
|
var BARE_URL = /^https?:\/\//u;
|
|
17587
|
+
function parseKitSelector(raw) {
|
|
17588
|
+
if (BARE_URL.test(raw)) {
|
|
17589
|
+
throw new Error(
|
|
17590
|
+
"A bare URL is not a kit reference. Configure source.url in tangyr.config.yaml and reference the kit as --kit <name> or --kit <name>@<version>."
|
|
17591
|
+
);
|
|
17592
|
+
}
|
|
17593
|
+
if (raw.length === 0) {
|
|
17594
|
+
throw new Error(
|
|
17595
|
+
"No kit was named. Pass --kit <name> to resolve the configured channel, or --kit <name>@<version> for an exact version."
|
|
17596
|
+
);
|
|
17597
|
+
}
|
|
17598
|
+
const separator = raw.lastIndexOf("@");
|
|
17599
|
+
if (separator === -1) {
|
|
17600
|
+
assertPathSafeComponent("name", raw);
|
|
17601
|
+
return { name: raw };
|
|
17602
|
+
}
|
|
17603
|
+
return parseKitReference(raw);
|
|
17604
|
+
}
|
|
17580
17605
|
function parseKitReference(raw) {
|
|
17581
17606
|
if (BARE_URL.test(raw)) {
|
|
17582
17607
|
throw new Error(
|
|
@@ -23116,17 +23141,94 @@ function toConflictPolicy2(value) {
|
|
|
23116
23141
|
import fs36 from "fs";
|
|
23117
23142
|
import path36 from "path";
|
|
23118
23143
|
|
|
23144
|
+
// src/core/channel-state.ts
|
|
23145
|
+
import fs28 from "fs";
|
|
23146
|
+
import path26 from "path";
|
|
23147
|
+
var CHANNEL_STATE_FILENAME = ".tangyr-channels.json";
|
|
23148
|
+
function stateFilePath(cacheRoot) {
|
|
23149
|
+
return path26.join(cacheRoot, CHANNEL_STATE_FILENAME);
|
|
23150
|
+
}
|
|
23151
|
+
function channelKey(kit, channel) {
|
|
23152
|
+
return `${kit}/${channel}`;
|
|
23153
|
+
}
|
|
23154
|
+
function readState(cacheRoot) {
|
|
23155
|
+
const filePath = stateFilePath(cacheRoot);
|
|
23156
|
+
let raw;
|
|
23157
|
+
try {
|
|
23158
|
+
raw = fs28.readFileSync(filePath, "utf8");
|
|
23159
|
+
} catch (err) {
|
|
23160
|
+
if (err.code === "ENOENT") {
|
|
23161
|
+
return { state: {} };
|
|
23162
|
+
}
|
|
23163
|
+
return {
|
|
23164
|
+
state: {},
|
|
23165
|
+
warning: `Could not read ${filePath} (${err.message}); the channel downgrade guard starts from nothing for this run.`
|
|
23166
|
+
};
|
|
23167
|
+
}
|
|
23168
|
+
let parsed;
|
|
23169
|
+
try {
|
|
23170
|
+
parsed = JSON.parse(raw);
|
|
23171
|
+
} catch {
|
|
23172
|
+
return {
|
|
23173
|
+
state: {},
|
|
23174
|
+
warning: `${filePath} is not valid JSON; the channel downgrade guard starts from nothing for this run.`
|
|
23175
|
+
};
|
|
23176
|
+
}
|
|
23177
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
23178
|
+
return {
|
|
23179
|
+
state: {},
|
|
23180
|
+
warning: `${filePath} does not contain an object; the channel downgrade guard starts from nothing for this run.`
|
|
23181
|
+
};
|
|
23182
|
+
}
|
|
23183
|
+
return { state: parsed };
|
|
23184
|
+
}
|
|
23185
|
+
function readChannelSequence(cacheRoot, originUrl, kit, channel) {
|
|
23186
|
+
const { state, warning } = readState(cacheRoot);
|
|
23187
|
+
const record = state[originUrl]?.[channelKey(kit, channel)];
|
|
23188
|
+
const usable = record !== void 0 && Number.isSafeInteger(record.sequence) && typeof record.version === "string" && record.version.length > 0;
|
|
23189
|
+
const result = usable ? { sequence: record.sequence, version: record.version } : { sequence: void 0, version: void 0 };
|
|
23190
|
+
return warning === void 0 ? result : { ...result, warning };
|
|
23191
|
+
}
|
|
23192
|
+
function writeChannelSequence(params) {
|
|
23193
|
+
const { cacheRoot, originUrl, kit, channel, sequence, version, now } = params;
|
|
23194
|
+
const { state } = readState(cacheRoot);
|
|
23195
|
+
const key = channelKey(kit, channel);
|
|
23196
|
+
const existing = state[originUrl]?.[key]?.sequence;
|
|
23197
|
+
if (existing !== void 0 && existing > sequence) {
|
|
23198
|
+
return;
|
|
23199
|
+
}
|
|
23200
|
+
state[originUrl] = {
|
|
23201
|
+
...state[originUrl],
|
|
23202
|
+
[key]: { sequence, version, seenAt: now.toISOString() }
|
|
23203
|
+
};
|
|
23204
|
+
const filePath = stateFilePath(cacheRoot);
|
|
23205
|
+
fs28.mkdirSync(path26.dirname(filePath), { recursive: true, mode: 448 });
|
|
23206
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
23207
|
+
fs28.writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}
|
|
23208
|
+
`, {
|
|
23209
|
+
mode: 384
|
|
23210
|
+
});
|
|
23211
|
+
fs28.renameSync(tempPath, filePath);
|
|
23212
|
+
}
|
|
23213
|
+
function assertChannelNotRolledBack(params) {
|
|
23214
|
+
const { originUrl, kit, channel, seen, seenVersion, incoming } = params;
|
|
23215
|
+
if (seen !== void 0 && incoming < seen) {
|
|
23216
|
+
throw new IntegrityError(
|
|
23217
|
+
`Channel pointer for ${kit}/${channel} from ${originUrl} carries sequence ${incoming}, behind the highest already accepted (${seen}${seenVersion === void 0 ? "" : `, naming ${kit}@${seenVersion}`}). That is a replay of a superseded pointer, which would pin this machine to ${kit}@${params.incomingVersion} and hide every version published since \u2014 refusing.`
|
|
23218
|
+
);
|
|
23219
|
+
}
|
|
23220
|
+
}
|
|
23221
|
+
|
|
23222
|
+
// src/core/bundle.ts
|
|
23223
|
+
var CHANNEL_POINTER_TYPE = "tangyr.channel-pointer.v1";
|
|
23224
|
+
var DEFAULT_CHANNEL = "stable";
|
|
23225
|
+
|
|
23119
23226
|
// src/core/materialize.ts
|
|
23120
23227
|
import fs29 from "fs";
|
|
23121
23228
|
import path27 from "path";
|
|
23122
23229
|
|
|
23123
23230
|
// src/core/integrity.ts
|
|
23124
23231
|
import crypto3 from "crypto";
|
|
23125
|
-
|
|
23126
|
-
// src/core/bundle.ts
|
|
23127
|
-
var DENY_LIST_TYPE = "tangyr.deny-list.v2";
|
|
23128
|
-
|
|
23129
|
-
// src/core/integrity.ts
|
|
23130
23232
|
function canonicalize(value) {
|
|
23131
23233
|
if (Array.isArray(value)) {
|
|
23132
23234
|
return value.map(canonicalize);
|
|
@@ -23241,43 +23343,7 @@ function verifyBundle(bundle, keys, expectedIdentity, now) {
|
|
|
23241
23343
|
verifyBundleSignature(bundle, keys, now);
|
|
23242
23344
|
verifyIdentity(bundle.manifest, expectedIdentity);
|
|
23243
23345
|
}
|
|
23244
|
-
function
|
|
23245
|
-
if (denyList === null || typeof denyList !== "object") {
|
|
23246
|
-
throw new IntegrityError(DENY_LIST_MALFORMED);
|
|
23247
|
-
}
|
|
23248
|
-
const actualKeys = Object.keys(denyList).sort();
|
|
23249
|
-
const expectedKeys = [
|
|
23250
|
-
"entries",
|
|
23251
|
-
"expiresAt",
|
|
23252
|
-
"issuedAt",
|
|
23253
|
-
"keyId",
|
|
23254
|
-
"sequence",
|
|
23255
|
-
"signature",
|
|
23256
|
-
"type"
|
|
23257
|
-
];
|
|
23258
|
-
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
23259
|
-
throw new IntegrityError(
|
|
23260
|
-
`${DENY_LIST_MALFORMED} Expected exactly [${expectedKeys.join(", ")}], got [${actualKeys.join(", ")}].`
|
|
23261
|
-
);
|
|
23262
|
-
}
|
|
23263
|
-
const list = denyList;
|
|
23264
|
-
const shapeOk = list.type === DENY_LIST_TYPE && typeof list.keyId === "string" && typeof list.signature === "string" && list.signature.length > 0 && Number.isSafeInteger(list.sequence) && list.sequence >= 0 && typeof list.issuedAt === "string" && typeof list.expiresAt === "string" && Array.isArray(list.entries) && list.entries.every((entry) => typeof entry === "string");
|
|
23265
|
-
if (!shapeOk) {
|
|
23266
|
-
throw new IntegrityError(DENY_LIST_MALFORMED);
|
|
23267
|
-
}
|
|
23268
|
-
const { signature, ...signed } = list;
|
|
23269
|
-
verifySignedPayload(
|
|
23270
|
-
canonicalJsonBytes(signed),
|
|
23271
|
-
signature,
|
|
23272
|
-
list.keyId,
|
|
23273
|
-
keys,
|
|
23274
|
-
now
|
|
23275
|
-
);
|
|
23276
|
-
assertFreshnessWindow(list.issuedAt, list.expiresAt, now);
|
|
23277
|
-
return list;
|
|
23278
|
-
}
|
|
23279
|
-
var DENY_LIST_MALFORMED = "Revocation deny-list response is malformed, unsigned, or corrupted \u2014 treated as an integrity failure, never as an absent deny-list (REQ-INT-009).";
|
|
23280
|
-
function assertFreshnessWindow(issuedAt, expiresAt, now) {
|
|
23346
|
+
function assertFreshnessWindow(issuedAt, expiresAt, now, expiryConsequence) {
|
|
23281
23347
|
const utcDesignatorPattern = /Z|[+-]\d{2}:\d{2}$/;
|
|
23282
23348
|
if (!utcDesignatorPattern.test(issuedAt) || !utcDesignatorPattern.test(expiresAt)) {
|
|
23283
23349
|
throw new IntegrityError(
|
|
@@ -23298,7 +23364,7 @@ function assertFreshnessWindow(issuedAt, expiresAt, now) {
|
|
|
23298
23364
|
}
|
|
23299
23365
|
if (now.getTime() > expires.getTime()) {
|
|
23300
23366
|
throw new IntegrityError(
|
|
23301
|
-
`Signed document expired at ${expiresAt} (now ${now.toISOString()}).
|
|
23367
|
+
`Signed document expired at ${expiresAt} (now ${now.toISOString()}). ${expiryConsequence}`
|
|
23302
23368
|
);
|
|
23303
23369
|
}
|
|
23304
23370
|
if (now.getTime() < issued.getTime()) {
|
|
@@ -23307,74 +23373,54 @@ function assertFreshnessWindow(issuedAt, expiresAt, now) {
|
|
|
23307
23373
|
);
|
|
23308
23374
|
}
|
|
23309
23375
|
}
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
var REVOCATION_STATE_FILENAME = ".tangyr-revocation.json";
|
|
23315
|
-
function stateFilePath(cacheRoot) {
|
|
23316
|
-
return path26.join(cacheRoot, REVOCATION_STATE_FILENAME);
|
|
23317
|
-
}
|
|
23318
|
-
function readState(cacheRoot) {
|
|
23319
|
-
const filePath = stateFilePath(cacheRoot);
|
|
23320
|
-
let raw;
|
|
23321
|
-
try {
|
|
23322
|
-
raw = fs28.readFileSync(filePath, "utf8");
|
|
23323
|
-
} catch (err) {
|
|
23324
|
-
if (err.code === "ENOENT") {
|
|
23325
|
-
return { state: {} };
|
|
23326
|
-
}
|
|
23327
|
-
return {
|
|
23328
|
-
state: {},
|
|
23329
|
-
warning: `Could not read ${filePath} (${err.message}); the deny-list replay guard starts from nothing for this run.`
|
|
23330
|
-
};
|
|
23331
|
-
}
|
|
23332
|
-
let parsed;
|
|
23333
|
-
try {
|
|
23334
|
-
parsed = JSON.parse(raw);
|
|
23335
|
-
} catch {
|
|
23336
|
-
return {
|
|
23337
|
-
state: {},
|
|
23338
|
-
warning: `${filePath} is not valid JSON; the deny-list replay guard starts from nothing for this run.`
|
|
23339
|
-
};
|
|
23376
|
+
var CHANNEL_POINTER_MALFORMED = "Channel pointer from the origin is malformed.";
|
|
23377
|
+
function verifyChannelPointer(pointer, expectedKit, expectedChannel, keys, now) {
|
|
23378
|
+
if (pointer === null || typeof pointer !== "object") {
|
|
23379
|
+
throw new IntegrityError(CHANNEL_POINTER_MALFORMED);
|
|
23340
23380
|
}
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
|
|
23344
|
-
|
|
23345
|
-
|
|
23381
|
+
const actualKeys = Object.keys(pointer).sort();
|
|
23382
|
+
const expectedKeys = [
|
|
23383
|
+
"channel",
|
|
23384
|
+
"expiresAt",
|
|
23385
|
+
"issuedAt",
|
|
23386
|
+
"keyId",
|
|
23387
|
+
"kit",
|
|
23388
|
+
"sequence",
|
|
23389
|
+
"signature",
|
|
23390
|
+
"type",
|
|
23391
|
+
"version"
|
|
23392
|
+
];
|
|
23393
|
+
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
23394
|
+
throw new IntegrityError(
|
|
23395
|
+
`${CHANNEL_POINTER_MALFORMED} Expected exactly [${expectedKeys.join(", ")}], got [${actualKeys.join(", ")}].`
|
|
23396
|
+
);
|
|
23346
23397
|
}
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
|
|
23351
|
-
const record = state[originUrl];
|
|
23352
|
-
const sequence = record !== void 0 && Number.isSafeInteger(record.sequence) ? record.sequence : void 0;
|
|
23353
|
-
return warning === void 0 ? { sequence } : { sequence, warning };
|
|
23354
|
-
}
|
|
23355
|
-
function writeRevocationSequence(cacheRoot, originUrl, sequence, now) {
|
|
23356
|
-
const { state } = readState(cacheRoot);
|
|
23357
|
-
const existing = state[originUrl]?.sequence;
|
|
23358
|
-
if (existing !== void 0 && existing >= sequence) {
|
|
23359
|
-
return;
|
|
23398
|
+
const candidate = pointer;
|
|
23399
|
+
const shapeOk = candidate.type === CHANNEL_POINTER_TYPE && typeof candidate.keyId === "string" && typeof candidate.kit === "string" && candidate.kit.length > 0 && typeof candidate.channel === "string" && candidate.channel.length > 0 && typeof candidate.version === "string" && candidate.version.length > 0 && typeof candidate.signature === "string" && candidate.signature.length > 0 && Number.isSafeInteger(candidate.sequence) && candidate.sequence >= 0 && typeof candidate.issuedAt === "string" && typeof candidate.expiresAt === "string";
|
|
23400
|
+
if (!shapeOk) {
|
|
23401
|
+
throw new IntegrityError(CHANNEL_POINTER_MALFORMED);
|
|
23360
23402
|
}
|
|
23361
|
-
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
}
|
|
23371
|
-
function assertSequenceNotReplayed(params) {
|
|
23372
|
-
const { originUrl, seen, incoming } = params;
|
|
23373
|
-
if (seen !== void 0 && incoming < seen) {
|
|
23403
|
+
const { signature, ...signed } = candidate;
|
|
23404
|
+
verifySignedPayload(
|
|
23405
|
+
canonicalJsonBytes(signed),
|
|
23406
|
+
signature,
|
|
23407
|
+
candidate.keyId,
|
|
23408
|
+
keys,
|
|
23409
|
+
now
|
|
23410
|
+
);
|
|
23411
|
+
if (candidate.kit !== expectedKit || candidate.channel !== expectedChannel) {
|
|
23374
23412
|
throw new IntegrityError(
|
|
23375
|
-
`
|
|
23413
|
+
`Channel pointer is signed for ${candidate.kit}/${candidate.channel} but was served as ${expectedKit}/${expectedChannel}. A pointer the publisher signed for one channel is being presented as another's \u2014 refusing.`
|
|
23376
23414
|
);
|
|
23377
23415
|
}
|
|
23416
|
+
assertPathSafeComponent("version", candidate.version);
|
|
23417
|
+
assertFreshnessWindow(
|
|
23418
|
+
candidate.issuedAt,
|
|
23419
|
+
candidate.expiresAt,
|
|
23420
|
+
now,
|
|
23421
|
+
"An expired channel pointer is not evidence that it still names the newest version \u2014 the publisher must re-sign and republish it."
|
|
23422
|
+
);
|
|
23423
|
+
return candidate;
|
|
23378
23424
|
}
|
|
23379
23425
|
|
|
23380
23426
|
// src/core/materialize.ts
|
|
@@ -23422,7 +23468,6 @@ async function withBoundedRetry(operation, sleep2, logger) {
|
|
|
23422
23468
|
);
|
|
23423
23469
|
}
|
|
23424
23470
|
var BUNDLE_OBJECT_PATH_PREFIX = "/v1/kits";
|
|
23425
|
-
var DENY_LIST_OBJECT_PATH = "/v1/deny-list";
|
|
23426
23471
|
async function fetchVerifiedBundle(params) {
|
|
23427
23472
|
const { originUrl, reference, transport, keys, now, logger, sleep: sleep2 } = params;
|
|
23428
23473
|
const credentials = resolveCredentialNonInteractive();
|
|
@@ -23476,76 +23521,6 @@ async function fetchVerifiedBundle(params) {
|
|
|
23476
23521
|
}
|
|
23477
23522
|
return parsed;
|
|
23478
23523
|
}
|
|
23479
|
-
async function assertReferenceNotRevoked(params) {
|
|
23480
|
-
const {
|
|
23481
|
-
originUrl,
|
|
23482
|
-
reference,
|
|
23483
|
-
cacheRoot,
|
|
23484
|
-
transport,
|
|
23485
|
-
keys,
|
|
23486
|
-
now,
|
|
23487
|
-
logger,
|
|
23488
|
-
sleep: sleep2
|
|
23489
|
-
} = params;
|
|
23490
|
-
const credentials = resolveCredentialNonInteractive();
|
|
23491
|
-
const denyList = await withBoundedRetry(
|
|
23492
|
-
async () => {
|
|
23493
|
-
const controller = new AbortController();
|
|
23494
|
-
const response = await transport.fetchObjectStream(
|
|
23495
|
-
originUrl,
|
|
23496
|
-
DENY_LIST_OBJECT_PATH,
|
|
23497
|
-
credentials,
|
|
23498
|
-
controller.signal
|
|
23499
|
-
);
|
|
23500
|
-
if (response.status === 401 || response.status === 403) {
|
|
23501
|
-
throw new AuthenticationError(
|
|
23502
|
-
`The origin rejected the credential (HTTP ${response.status}) fetching the revocation deny-list. Set TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET, or run 'tangyr auth login'.`
|
|
23503
|
-
);
|
|
23504
|
-
}
|
|
23505
|
-
if (response.status !== 200) {
|
|
23506
|
-
throw new Error(
|
|
23507
|
-
`Unexpected HTTP ${response.status} fetching the revocation deny-list from ${originUrl}${DENY_LIST_OBJECT_PATH}.`
|
|
23508
|
-
);
|
|
23509
|
-
}
|
|
23510
|
-
const buffer = await readBoundedStream(
|
|
23511
|
-
response.body,
|
|
23512
|
-
MAX_TOTAL_BUNDLE_BYTES,
|
|
23513
|
-
() => controller.abort()
|
|
23514
|
-
);
|
|
23515
|
-
let parsed;
|
|
23516
|
-
try {
|
|
23517
|
-
parsed = JSON.parse(buffer.toString("utf8"));
|
|
23518
|
-
} catch {
|
|
23519
|
-
throw new IntegrityError(
|
|
23520
|
-
"Revocation deny-list response is not valid JSON \u2014 rejecting (REQ-INT-009)."
|
|
23521
|
-
);
|
|
23522
|
-
}
|
|
23523
|
-
return verifyDenyList(parsed, keys, now);
|
|
23524
|
-
},
|
|
23525
|
-
sleep2,
|
|
23526
|
-
logger
|
|
23527
|
-
);
|
|
23528
|
-
const seen = readRevocationSequence(cacheRoot, originUrl);
|
|
23529
|
-
if (seen.warning) {
|
|
23530
|
-
logger.warn(seen.warning);
|
|
23531
|
-
}
|
|
23532
|
-
assertSequenceNotReplayed({
|
|
23533
|
-
originUrl,
|
|
23534
|
-
seen: seen.sequence,
|
|
23535
|
-
incoming: denyList.sequence
|
|
23536
|
-
});
|
|
23537
|
-
writeRevocationSequence(cacheRoot, originUrl, denyList.sequence, now);
|
|
23538
|
-
const key = `${reference.name}@${reference.version}`;
|
|
23539
|
-
if (denyList.entries.includes(key)) {
|
|
23540
|
-
const entryDir = cacheEntryPath(cacheRoot, reference);
|
|
23541
|
-
if (fs29.existsSync(entryDir)) {
|
|
23542
|
-
fs29.rmSync(entryDir, { recursive: true, force: true });
|
|
23543
|
-
}
|
|
23544
|
-
throw new IntegrityError(
|
|
23545
|
-
`"${key}" is present on the signed revocation deny-list \u2014 refusing to materialize or install; any existing cache entry has been invalidated (REQ-INT-009).`
|
|
23546
|
-
);
|
|
23547
|
-
}
|
|
23548
|
-
}
|
|
23549
23524
|
function parseVersionTriple(version) {
|
|
23550
23525
|
const leading = version.split("-")[0];
|
|
23551
23526
|
const parts = leading.split(".");
|
|
@@ -23716,16 +23691,6 @@ async function materializeRemoteKit(params) {
|
|
|
23716
23691
|
logger,
|
|
23717
23692
|
sleep: sleep2
|
|
23718
23693
|
});
|
|
23719
|
-
await assertReferenceNotRevoked({
|
|
23720
|
-
originUrl,
|
|
23721
|
-
reference,
|
|
23722
|
-
cacheRoot,
|
|
23723
|
-
transport,
|
|
23724
|
-
keys,
|
|
23725
|
-
now,
|
|
23726
|
-
logger,
|
|
23727
|
-
sleep: sleep2
|
|
23728
|
-
});
|
|
23729
23694
|
assertCliVersionCompatible(bundle.manifest.minCliVersion, CLI_VERSION);
|
|
23730
23695
|
const decoded = verifyBundleFileHashes(bundle);
|
|
23731
23696
|
if (decoded.has(CACHE_COMPLETION_MARKER) || decoded.has(BUNDLE_DOCUMENT_FILENAME)) {
|
|
@@ -23752,6 +23717,91 @@ async function materializeRemoteKit(params) {
|
|
|
23752
23717
|
throw error;
|
|
23753
23718
|
}
|
|
23754
23719
|
}
|
|
23720
|
+
var CHANNEL_OBJECT_PATH_PREFIX = "/v1/channels";
|
|
23721
|
+
var MAX_CHANNEL_POINTER_BYTES = 64 * 1024;
|
|
23722
|
+
async function resolveChannelVersion(params) {
|
|
23723
|
+
const {
|
|
23724
|
+
originUrl,
|
|
23725
|
+
kit,
|
|
23726
|
+
channel,
|
|
23727
|
+
cacheRoot,
|
|
23728
|
+
transport,
|
|
23729
|
+
keys,
|
|
23730
|
+
now,
|
|
23731
|
+
logger,
|
|
23732
|
+
sleep: sleep2
|
|
23733
|
+
} = params;
|
|
23734
|
+
const credentials = resolveCredentialNonInteractive();
|
|
23735
|
+
const objectPath = `${CHANNEL_OBJECT_PATH_PREFIX}/${kit}/${channel}.json`;
|
|
23736
|
+
const pointer = await withBoundedRetry(
|
|
23737
|
+
async () => {
|
|
23738
|
+
const controller = new AbortController();
|
|
23739
|
+
const response = await transport.fetchObjectStream(
|
|
23740
|
+
originUrl,
|
|
23741
|
+
objectPath,
|
|
23742
|
+
credentials,
|
|
23743
|
+
controller.signal
|
|
23744
|
+
);
|
|
23745
|
+
if (response.status === 401 || response.status === 403) {
|
|
23746
|
+
throw new AuthenticationError(
|
|
23747
|
+
`The origin rejected the credential (HTTP ${response.status}) resolving ${kit}/${channel}. Set TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET, or run 'tangyr auth login'.`
|
|
23748
|
+
);
|
|
23749
|
+
}
|
|
23750
|
+
if (response.status === 404) {
|
|
23751
|
+
throw new IntegrityError(
|
|
23752
|
+
`The origin has no ${channel} channel for "${kit}" (HTTP 404 at ${originUrl}${objectPath}). Either the publisher has not published that channel, or the kit name is wrong. Reference an explicit version as --kit ${kit}@<version> if you know one.`
|
|
23753
|
+
);
|
|
23754
|
+
}
|
|
23755
|
+
if (response.status !== 200) {
|
|
23756
|
+
throw new Error(
|
|
23757
|
+
`Unexpected HTTP ${response.status} resolving ${kit}/${channel} from ${originUrl}${objectPath}.`
|
|
23758
|
+
);
|
|
23759
|
+
}
|
|
23760
|
+
const buffer = await readBoundedStream(
|
|
23761
|
+
response.body,
|
|
23762
|
+
MAX_CHANNEL_POINTER_BYTES,
|
|
23763
|
+
() => controller.abort()
|
|
23764
|
+
);
|
|
23765
|
+
let parsed;
|
|
23766
|
+
try {
|
|
23767
|
+
parsed = JSON.parse(buffer.toString("utf8"));
|
|
23768
|
+
} catch {
|
|
23769
|
+
throw new IntegrityError(
|
|
23770
|
+
`Channel pointer for ${kit}/${channel} is not valid JSON \u2014 rejecting.`
|
|
23771
|
+
);
|
|
23772
|
+
}
|
|
23773
|
+
return verifyChannelPointer(parsed, kit, channel, keys, now);
|
|
23774
|
+
},
|
|
23775
|
+
sleep2,
|
|
23776
|
+
logger
|
|
23777
|
+
);
|
|
23778
|
+
const seen = readChannelSequence(cacheRoot, originUrl, kit, channel);
|
|
23779
|
+
if (seen.warning) {
|
|
23780
|
+
logger.warn(seen.warning);
|
|
23781
|
+
}
|
|
23782
|
+
assertChannelNotRolledBack({
|
|
23783
|
+
originUrl,
|
|
23784
|
+
kit,
|
|
23785
|
+
channel,
|
|
23786
|
+
seen: seen.sequence,
|
|
23787
|
+
seenVersion: seen.version,
|
|
23788
|
+
incoming: pointer.sequence,
|
|
23789
|
+
incomingVersion: pointer.version
|
|
23790
|
+
});
|
|
23791
|
+
writeChannelSequence({
|
|
23792
|
+
cacheRoot,
|
|
23793
|
+
originUrl,
|
|
23794
|
+
kit,
|
|
23795
|
+
channel,
|
|
23796
|
+
sequence: pointer.sequence,
|
|
23797
|
+
version: pointer.version,
|
|
23798
|
+
now
|
|
23799
|
+
});
|
|
23800
|
+
logger.verbose(
|
|
23801
|
+
`Resolved ${kit}/${channel} to ${kit}@${pointer.version} (pointer sequence ${pointer.sequence}).`
|
|
23802
|
+
);
|
|
23803
|
+
return pointer.version;
|
|
23804
|
+
}
|
|
23755
23805
|
|
|
23756
23806
|
// src/core/kit-source.ts
|
|
23757
23807
|
function resolveCacheOnly(entryDir, reference, keys, now) {
|
|
@@ -23790,13 +23840,25 @@ async function resolveKitSource(params) {
|
|
|
23790
23840
|
if (config.source.type !== "remote") {
|
|
23791
23841
|
return { kitName: kitOption };
|
|
23792
23842
|
}
|
|
23793
|
-
const
|
|
23843
|
+
const selector = parseKitSelector(kitOption ?? "");
|
|
23794
23844
|
if (!config.source.url) {
|
|
23795
23845
|
throw new Error(
|
|
23796
23846
|
"Configuration declares source.type: remote but no source.url. Set source.url to the https endpoint of your kit origin."
|
|
23797
23847
|
);
|
|
23798
23848
|
}
|
|
23799
23849
|
const cacheRoot = resolveCacheRoot(env2);
|
|
23850
|
+
const reference = selector.version === void 0 ? await resolveThroughChannel({
|
|
23851
|
+
name: selector.name,
|
|
23852
|
+
channel: config.source.channel ?? DEFAULT_CHANNEL,
|
|
23853
|
+
originUrl: config.source.url,
|
|
23854
|
+
cacheRoot,
|
|
23855
|
+
offline: offline === true,
|
|
23856
|
+
transport,
|
|
23857
|
+
keys,
|
|
23858
|
+
now,
|
|
23859
|
+
logger,
|
|
23860
|
+
sleep: sleep2
|
|
23861
|
+
}) : { name: selector.name, version: selector.version };
|
|
23800
23862
|
const entryDir = cacheEntryPath(cacheRoot, reference);
|
|
23801
23863
|
if (offline) {
|
|
23802
23864
|
return resolveCacheOnly(entryDir, reference, keys, now);
|
|
@@ -23827,7 +23889,7 @@ async function resolveKitSource(params) {
|
|
|
23827
23889
|
}
|
|
23828
23890
|
function degradedWarmReturn() {
|
|
23829
23891
|
process.stderr.write(
|
|
23830
|
-
`Degraded: proceeding on the warm cache for "${reference.name}@${reference.version}" after a network failure during the authorization-freshness re-check \u2014
|
|
23892
|
+
`Degraded: proceeding on the warm cache for "${reference.name}@${reference.version}" after a network failure during the authorization-freshness re-check \u2014 freshness was not re-checked. Run with --refresh once connectivity is restored.
|
|
23831
23893
|
`
|
|
23832
23894
|
);
|
|
23833
23895
|
return {
|
|
@@ -23869,23 +23931,6 @@ async function resolveKitSource(params) {
|
|
|
23869
23931
|
);
|
|
23870
23932
|
}
|
|
23871
23933
|
if (probeOutcome.outcome === "success") {
|
|
23872
|
-
try {
|
|
23873
|
-
await assertReferenceNotRevoked({
|
|
23874
|
-
originUrl: config.source.url,
|
|
23875
|
-
reference,
|
|
23876
|
-
cacheRoot,
|
|
23877
|
-
transport,
|
|
23878
|
-
keys,
|
|
23879
|
-
now,
|
|
23880
|
-
logger,
|
|
23881
|
-
sleep: sleep2
|
|
23882
|
-
});
|
|
23883
|
-
} catch (error) {
|
|
23884
|
-
if (!(error instanceof RemoteUnavailableError)) {
|
|
23885
|
-
throw error;
|
|
23886
|
-
}
|
|
23887
|
-
return degradedWarmReturn();
|
|
23888
|
-
}
|
|
23889
23934
|
writeLastAuthorizedAt(cacheRoot, config.source.url, now);
|
|
23890
23935
|
const warning = formatAdvanceExpiryWarning(
|
|
23891
23936
|
probeOutcome.metadata.expiresAt,
|
|
@@ -23934,6 +23979,54 @@ async function resolveKitSource(params) {
|
|
|
23934
23979
|
writeLastAuthorizedAt(cacheRoot, config.source.url, now);
|
|
23935
23980
|
return { kitPathOverride, kitName: reference.name };
|
|
23936
23981
|
}
|
|
23982
|
+
async function resolveThroughChannel(params) {
|
|
23983
|
+
const { name, channel, originUrl, cacheRoot, offline, logger } = params;
|
|
23984
|
+
const lastSeenVersion = () => {
|
|
23985
|
+
const seen = readChannelSequence(cacheRoot, originUrl, name, channel);
|
|
23986
|
+
if (seen.warning) {
|
|
23987
|
+
logger.warn(seen.warning);
|
|
23988
|
+
}
|
|
23989
|
+
return seen.version;
|
|
23990
|
+
};
|
|
23991
|
+
if (offline) {
|
|
23992
|
+
const version = lastSeenVersion();
|
|
23993
|
+
if (version === void 0) {
|
|
23994
|
+
throw new OfflineCacheMissError(
|
|
23995
|
+
`--offline cannot resolve ${name}/${channel}: this machine has never resolved that channel, and a channel pointer is never cached. Run once with network access, or name a version as --kit ${name}@<version>.`
|
|
23996
|
+
);
|
|
23997
|
+
}
|
|
23998
|
+
logger.warn(
|
|
23999
|
+
`--offline: using ${name}@${version}, the last version this machine saw on ${channel}. A newer one may exist.`
|
|
24000
|
+
);
|
|
24001
|
+
return { name, version };
|
|
24002
|
+
}
|
|
24003
|
+
try {
|
|
24004
|
+
const version = await resolveChannelVersion({
|
|
24005
|
+
originUrl,
|
|
24006
|
+
kit: name,
|
|
24007
|
+
channel,
|
|
24008
|
+
cacheRoot,
|
|
24009
|
+
transport: params.transport,
|
|
24010
|
+
keys: params.keys,
|
|
24011
|
+
now: params.now,
|
|
24012
|
+
logger,
|
|
24013
|
+
sleep: params.sleep
|
|
24014
|
+
});
|
|
24015
|
+
return { name, version };
|
|
24016
|
+
} catch (error) {
|
|
24017
|
+
if (!(error instanceof RemoteUnavailableError)) {
|
|
24018
|
+
throw error;
|
|
24019
|
+
}
|
|
24020
|
+
const version = lastSeenVersion();
|
|
24021
|
+
if (version === void 0) {
|
|
24022
|
+
throw error;
|
|
24023
|
+
}
|
|
24024
|
+
logger.warn(
|
|
24025
|
+
`Could not reach the origin to resolve ${name}/${channel}; falling back to ${name}@${version}, the last version this machine saw. A newer one may exist.`
|
|
24026
|
+
);
|
|
24027
|
+
return { name, version };
|
|
24028
|
+
}
|
|
24029
|
+
}
|
|
23937
24030
|
|
|
23938
24031
|
// src/core/keys.ts
|
|
23939
24032
|
var PINNED_PUBLISHER_KEYS = [
|