spfn 0.3.0-beta.4 → 0.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -3
- package/dist/index.d.ts +1059 -1
- package/dist/index.js +6983 -242
- package/dist/templates/vercel/npmrc +0 -3
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,1064 @@ interface SpfnConfig {
|
|
|
117
117
|
deployment: DeploymentConfig;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
type KitGate = 'kit-check' | 'typecheck' | 'test' | 'build' | 'db-status' | 'health';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Turning a release's scaffold archive into a project on disk.
|
|
124
|
+
*
|
|
125
|
+
* The archive is one artifact and it is verified as one: the manifest declares
|
|
126
|
+
* an integrity for it, and nothing is expanded until the bytes that arrived
|
|
127
|
+
* match. That ordering is the whole safety property — a half-expanded archive
|
|
128
|
+
* that turns out to be wrong leaves files a later step has to guess about,
|
|
129
|
+
* while an archive rejected before expansion leaves the directory exactly as it
|
|
130
|
+
* was found.
|
|
131
|
+
*
|
|
132
|
+
* Two things the expansion will not do. It will not write outside the directory
|
|
133
|
+
* it was given, which `readTar` enforces by refusing the paths that could. And
|
|
134
|
+
* it will not overwrite a file that is already there: by the time the scaffold
|
|
135
|
+
* runs, activation has already written the licence file, and an archive that
|
|
136
|
+
* claims a path something else owns is a release bug, not a merge to resolve.
|
|
137
|
+
*
|
|
138
|
+
* The seed is the small, customer-owned part: the project's own name. It is
|
|
139
|
+
* written after expansion rather than baked into the archive so that one
|
|
140
|
+
* archive serves every project, and it is the only file this module edits
|
|
141
|
+
* rather than creates.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/** What the manifest says about the release's scaffold. */
|
|
145
|
+
interface KitScaffoldDescriptor {
|
|
146
|
+
recipeVersion: string;
|
|
147
|
+
artifact: string;
|
|
148
|
+
integrity: string;
|
|
149
|
+
}
|
|
150
|
+
interface ScaffoldMaterializerOptions {
|
|
151
|
+
artifacts: ArtifactPort;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The real `ScaffoldPort`: fetch, verify, expand, seed.
|
|
155
|
+
*
|
|
156
|
+
* `spfn create` is not shelled out to. A scaffold that came from a signed
|
|
157
|
+
* release has to be the *release's* scaffold, and a locally installed CLI
|
|
158
|
+
* generating its own approximation of one is how a project ends up subtly
|
|
159
|
+
* unlike the release it claims to be.
|
|
160
|
+
*/
|
|
161
|
+
declare class ArtifactScaffoldPort implements ScaffoldPort {
|
|
162
|
+
private readonly artifacts;
|
|
163
|
+
constructor(options: ScaffoldMaterializerOptions);
|
|
164
|
+
createBase(request: {
|
|
165
|
+
targetDir: string;
|
|
166
|
+
name: string;
|
|
167
|
+
scaffold?: KitScaffoldDescriptor;
|
|
168
|
+
}): Promise<void>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Where a Kit's local credential lives, and the two-step dance that puts it
|
|
173
|
+
* there (unit 06 section 3.3).
|
|
174
|
+
*
|
|
175
|
+
* The credential is stored in the OS keychain under a service of its own,
|
|
176
|
+
* `superfunction.spfn.kit`, so it is never reachable through the env-secret
|
|
177
|
+
* path that `spfn secret` uses. What is committed to the repository is only
|
|
178
|
+
* enough to *find* the item again — kit ID, activation ID, client ID — never
|
|
179
|
+
* the value and never the expiry, which changes on every rotation and would
|
|
180
|
+
* make the committed file wrong minutes after it was written.
|
|
181
|
+
*
|
|
182
|
+
* The dance matters. A first activation has no activation ID yet, so the CLI
|
|
183
|
+
* generates the credential locally, writes it under a `pending` account, and
|
|
184
|
+
* only then calls activation. If the response is lost in the network the same
|
|
185
|
+
* pending value re-drives the same idempotent call, and a second slot is never
|
|
186
|
+
* consumed. Once the server answers, the item moves to its final account. If
|
|
187
|
+
* the license is refused, the pending item is deleted.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
interface KitCredentialRecord {
|
|
191
|
+
/** The opaque local credential the control plane issued or accepted. */
|
|
192
|
+
credential: string;
|
|
193
|
+
/** When registry access from this credential stops working. */
|
|
194
|
+
accessExpiresAt: string;
|
|
195
|
+
/** Server generation. A lower one than the server's is stale. */
|
|
196
|
+
generation: number;
|
|
197
|
+
}
|
|
198
|
+
interface KitCredentialIdentity {
|
|
199
|
+
kitId: string;
|
|
200
|
+
localClientId: string;
|
|
201
|
+
/** Present once the server has answered an activation. */
|
|
202
|
+
activationId?: string;
|
|
203
|
+
/** Present only while the first activation is still in flight. */
|
|
204
|
+
installationId?: string;
|
|
205
|
+
}
|
|
206
|
+
/** Identity of the item held while a first activation is in flight. */
|
|
207
|
+
type PendingIdentity = Required<Pick<KitCredentialIdentity, 'kitId' | 'installationId' | 'localClientId'>>;
|
|
208
|
+
/** Identity of the settled item, once the server has answered. */
|
|
209
|
+
type SettledIdentity = Required<Pick<KitCredentialIdentity, 'kitId' | 'activationId' | 'localClientId'>>;
|
|
210
|
+
interface KitCredentialStore {
|
|
211
|
+
readonly id: string;
|
|
212
|
+
isAvailable(): Promise<boolean>;
|
|
213
|
+
readPending(identity: PendingIdentity): Promise<KitCredentialRecord | null>;
|
|
214
|
+
savePending(identity: PendingIdentity, record: KitCredentialRecord): Promise<void>;
|
|
215
|
+
/** Move the pending item to its final account. Returns what was moved. */
|
|
216
|
+
promote(identity: Required<KitCredentialIdentity>, record?: KitCredentialRecord): Promise<KitCredentialRecord>;
|
|
217
|
+
read(identity: SettledIdentity): Promise<KitCredentialRecord | null>;
|
|
218
|
+
save(identity: SettledIdentity, record: KitCredentialRecord): Promise<void>;
|
|
219
|
+
remove(account: string): Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A public key the CLI is willing to trust, as base64 SPKI DER. */
|
|
223
|
+
interface TrustedKey {
|
|
224
|
+
keyId: string;
|
|
225
|
+
publicKey: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The first thing `spfn kit install` does, and the only place a setup URL is
|
|
230
|
+
* turned into something the CLI will act on.
|
|
231
|
+
*
|
|
232
|
+
* Unit 06 section 3.1: the setup link is a public locator, not a secret. That
|
|
233
|
+
* cuts both ways — because it is public it may be shared, and because it is
|
|
234
|
+
* public it must carry nothing else, so a URL with a query or a fragment is
|
|
235
|
+
* refused before a single byte is fetched. The origin is an exact allowlist:
|
|
236
|
+
* a valid signature on a descriptor served from somewhere else does not make
|
|
237
|
+
* that somewhere else an official setup origin.
|
|
238
|
+
*
|
|
239
|
+
* Order matters here, and it is the order below: allowlist, fetch, signature,
|
|
240
|
+
* schema, self-consistency, freshness, payload digest, CLI compatibility. Each
|
|
241
|
+
* step may only trust what the steps above it have already established.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
/** Fetches one URL and returns the parsed JSON body, or the redirect target. */
|
|
245
|
+
interface SetupFetchResult {
|
|
246
|
+
/** Set when the origin answered with a redirect. */
|
|
247
|
+
redirectTo?: string;
|
|
248
|
+
body?: unknown;
|
|
249
|
+
}
|
|
250
|
+
type SetupFetcher = (url: string) => Promise<SetupFetchResult>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Everything a Kit operation needs from the outside world, as one set of
|
|
254
|
+
* interfaces.
|
|
255
|
+
*
|
|
256
|
+
* The operations in `operations/` contain the order of the steps, the
|
|
257
|
+
* checkpoints and the refusals — the parts that are the same whatever is on the
|
|
258
|
+
* other end. The ports are the parts that are not: a control plane, a registry,
|
|
259
|
+
* a package manager, a database, Git, a cloud provider.
|
|
260
|
+
*
|
|
261
|
+
* The reason for the seam is not testability alone. It is that a Wave-1 install
|
|
262
|
+
* has to be provable *before* any of those services may be touched: with fake
|
|
263
|
+
* ports the whole sequence, including its failures and resumes, runs on a
|
|
264
|
+
* temporary directory and reaches nothing real.
|
|
265
|
+
*
|
|
266
|
+
* Two rules hold across every port:
|
|
267
|
+
* - a port reports outcomes, it does not decide policy. `activate` says
|
|
268
|
+
* `project-limit`; whether that is fatal is the operation's call;
|
|
269
|
+
* - a secret crosses a port boundary only as a value the CLI immediately puts
|
|
270
|
+
* into a keychain item or a child environment. No port writes one to disk.
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
interface KitClock {
|
|
274
|
+
/** An ISO-8601 instant with second precision, matching the contract. */
|
|
275
|
+
now(): string;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Transport for the signed documents. Each call returns the *signed wrapper*,
|
|
279
|
+
* never a document the transport has already decided to trust — verification is
|
|
280
|
+
* the CLI's job and stays in one place.
|
|
281
|
+
*/
|
|
282
|
+
interface CatalogPort {
|
|
283
|
+
fetchSignedCatalog(url: string): Promise<unknown>;
|
|
284
|
+
fetchSignedManifest(url: string): Promise<unknown>;
|
|
285
|
+
}
|
|
286
|
+
interface ActivationRequest {
|
|
287
|
+
kitId: string;
|
|
288
|
+
installationId: string;
|
|
289
|
+
localClientId: string;
|
|
290
|
+
/** Read from a masked prompt or stdin, held in memory, never stored. */
|
|
291
|
+
licenseKey: string;
|
|
292
|
+
/** The client-generated credential already parked in the pending item. */
|
|
293
|
+
candidateCredential: string;
|
|
294
|
+
}
|
|
295
|
+
interface ActivationResult {
|
|
296
|
+
status: 'activated' | 'license-invalid' | 'license-revoked' | 'project-limit' | 'unavailable';
|
|
297
|
+
activationId?: string;
|
|
298
|
+
accessExpiresAt?: string;
|
|
299
|
+
generation?: number;
|
|
300
|
+
/** Secret-free detail for the report, e.g. how many slots are in use. */
|
|
301
|
+
detail?: Record<string, string | number | boolean | null>;
|
|
302
|
+
}
|
|
303
|
+
interface EntitlementResult {
|
|
304
|
+
entitled: boolean;
|
|
305
|
+
reason?: 'expired' | 'not-entitled' | 'revoked' | 'unavailable';
|
|
306
|
+
}
|
|
307
|
+
interface EntitlementRequest {
|
|
308
|
+
activationId: string;
|
|
309
|
+
kitId: string;
|
|
310
|
+
release: string;
|
|
311
|
+
/**
|
|
312
|
+
* The package whose availability answers the question, from the release
|
|
313
|
+
* that is being asked about.
|
|
314
|
+
*
|
|
315
|
+
* A generic CLI cannot know what a Kit ships as. Guessing it from the Kit
|
|
316
|
+
* id works only for Kits whose package happens to be named after them, and
|
|
317
|
+
* a wrong guess is answered by the registry as "not entitled" — a refusal
|
|
318
|
+
* the customer reads as a licence problem when nothing about the licence
|
|
319
|
+
* is wrong. The manifest names its packages, so the caller that holds the
|
|
320
|
+
* manifest says which one to ask about, and this stays a transport.
|
|
321
|
+
*/
|
|
322
|
+
packageName?: string;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The answer to "please e-mail this activation a recovery challenge".
|
|
326
|
+
*
|
|
327
|
+
* `sent` whether or not the activation exists. The control plane answers the
|
|
328
|
+
* same way in both cases on purpose (unit 04 §6.1), so a caller who could tell
|
|
329
|
+
* the difference would be telling an attacker which public activation ids are
|
|
330
|
+
* real. The CLI carries that property forward rather than trying to be more
|
|
331
|
+
* helpful than the service it asked.
|
|
332
|
+
*/
|
|
333
|
+
interface RecoveryRequestResult {
|
|
334
|
+
status: 'sent' | 'unavailable';
|
|
335
|
+
detail?: Record<string, string | number | boolean | null>;
|
|
336
|
+
}
|
|
337
|
+
interface RecoveryCompletionResult {
|
|
338
|
+
status: 'recovered' | 'recovery-invalid' | 'license-revoked' | 'unavailable';
|
|
339
|
+
/** The activation the replacement credential now belongs to. */
|
|
340
|
+
activationId?: string;
|
|
341
|
+
/** The server's name for the client the replacement created. */
|
|
342
|
+
localClientId?: string;
|
|
343
|
+
accessExpiresAt?: string;
|
|
344
|
+
generation?: number;
|
|
345
|
+
detail?: Record<string, string | number | boolean | null>;
|
|
346
|
+
}
|
|
347
|
+
interface LicensePort {
|
|
348
|
+
activate(request: ActivationRequest): Promise<ActivationResult>;
|
|
349
|
+
entitlement(request: EntitlementRequest): Promise<EntitlementResult>;
|
|
350
|
+
/** Unit 06 §4.10 step 1: ask for a challenge at the address of record. */
|
|
351
|
+
requestRecovery(request: {
|
|
352
|
+
activationId: string;
|
|
353
|
+
}): Promise<RecoveryRequestResult>;
|
|
354
|
+
/**
|
|
355
|
+
* Unit 06 §4.10 steps 4–5.
|
|
356
|
+
*
|
|
357
|
+
* The replacement credential is generated by the CLI and sent, rather than
|
|
358
|
+
* minted by the server and returned: a secret the server mints can be lost
|
|
359
|
+
* in transit, and losing the one that replaces a lost one has no next step.
|
|
360
|
+
*/
|
|
361
|
+
completeRecovery(request: {
|
|
362
|
+
recoveryId: string;
|
|
363
|
+
challenge: string;
|
|
364
|
+
replacementCredential: string;
|
|
365
|
+
}): Promise<RecoveryCompletionResult>;
|
|
366
|
+
}
|
|
367
|
+
interface RegistrySession {
|
|
368
|
+
status: 'ok' | 'credential-stale' | 'credential-invalid' | 'unavailable';
|
|
369
|
+
/** Short-lived bearer, handed only to a child environment. */
|
|
370
|
+
token?: string;
|
|
371
|
+
expiresInSeconds?: number;
|
|
372
|
+
}
|
|
373
|
+
interface RegistryPort {
|
|
374
|
+
/**
|
|
375
|
+
* `kitId` names the keychain item the credential lives in, so a port that
|
|
376
|
+
* rotates an expired credential can put the replacement back where the next
|
|
377
|
+
* command will look for it. A port that never rotates ignores it.
|
|
378
|
+
*/
|
|
379
|
+
issueSession(request: {
|
|
380
|
+
kitId?: string;
|
|
381
|
+
activationId: string;
|
|
382
|
+
localClientId: string;
|
|
383
|
+
credential: string;
|
|
384
|
+
/**
|
|
385
|
+
* Set on a retry after the registry refused the last credential. The
|
|
386
|
+
* local record can look perfectly current while the server has already
|
|
387
|
+
* stopped honouring it, and re-presenting it would fail identically.
|
|
388
|
+
*/
|
|
389
|
+
forceRotation?: boolean;
|
|
390
|
+
}): Promise<RegistrySession>;
|
|
391
|
+
}
|
|
392
|
+
interface PackageInstallResult {
|
|
393
|
+
ok: boolean;
|
|
394
|
+
exitCode: number;
|
|
395
|
+
/** Why it failed, in terms the operation can branch on. */
|
|
396
|
+
failure?: 'unauthorized' | 'network' | 'resolution' | 'other';
|
|
397
|
+
}
|
|
398
|
+
interface PackageManagerPort {
|
|
399
|
+
install(request: {
|
|
400
|
+
cwd: string;
|
|
401
|
+
frozen: boolean;
|
|
402
|
+
env: Record<string, string>;
|
|
403
|
+
}): Promise<PackageInstallResult>;
|
|
404
|
+
}
|
|
405
|
+
interface DatabaseStatus {
|
|
406
|
+
/** Whether a connection string is configured at all. */
|
|
407
|
+
configured: boolean;
|
|
408
|
+
reachable: boolean;
|
|
409
|
+
applied: string[];
|
|
410
|
+
pending: string[];
|
|
411
|
+
}
|
|
412
|
+
interface MigrationResult {
|
|
413
|
+
ok: boolean;
|
|
414
|
+
applied: string[];
|
|
415
|
+
pending: string[];
|
|
416
|
+
backupId?: string;
|
|
417
|
+
failure?: string;
|
|
418
|
+
}
|
|
419
|
+
interface DatabasePort {
|
|
420
|
+
status(request: {
|
|
421
|
+
cwd: string;
|
|
422
|
+
}): Promise<DatabaseStatus>;
|
|
423
|
+
migrate(request: {
|
|
424
|
+
cwd: string;
|
|
425
|
+
withBackup: boolean;
|
|
426
|
+
}): Promise<MigrationResult>;
|
|
427
|
+
}
|
|
428
|
+
interface GateResult {
|
|
429
|
+
ok: boolean;
|
|
430
|
+
/** A short secret-free summary; never raw command output. */
|
|
431
|
+
summary?: string;
|
|
432
|
+
}
|
|
433
|
+
interface GatePort {
|
|
434
|
+
run(gate: KitGate, request: {
|
|
435
|
+
cwd: string;
|
|
436
|
+
}): Promise<GateResult>;
|
|
437
|
+
}
|
|
438
|
+
interface GitPort {
|
|
439
|
+
init(request: {
|
|
440
|
+
cwd: string;
|
|
441
|
+
}): Promise<void>;
|
|
442
|
+
isClean(request: {
|
|
443
|
+
cwd: string;
|
|
444
|
+
}): Promise<boolean>;
|
|
445
|
+
commit(request: {
|
|
446
|
+
cwd: string;
|
|
447
|
+
message: string;
|
|
448
|
+
}): Promise<{
|
|
449
|
+
commit: string;
|
|
450
|
+
}>;
|
|
451
|
+
head(request: {
|
|
452
|
+
cwd: string;
|
|
453
|
+
}): Promise<string | null>;
|
|
454
|
+
/**
|
|
455
|
+
* Which of these paths the repository actually tracks.
|
|
456
|
+
*
|
|
457
|
+
* Asked because a file that exists is not the same as a file a clean clone
|
|
458
|
+
* will have. `.spfn/kit-lock.json` and `.spfn/license.json` are the two
|
|
459
|
+
* files a restore has nothing without (unit 06 §4.2, §5.1), and a scaffold
|
|
460
|
+
* whose `.gitignore` covers `.spfn/` produces a project that installs,
|
|
461
|
+
* builds, passes every gate and cannot be restored from its own repository.
|
|
462
|
+
*/
|
|
463
|
+
trackedAmong(request: {
|
|
464
|
+
cwd: string;
|
|
465
|
+
paths: readonly string[];
|
|
466
|
+
}): Promise<string[]>;
|
|
467
|
+
}
|
|
468
|
+
interface ScaffoldPort {
|
|
469
|
+
/**
|
|
470
|
+
* The project's base, as the release publishes it.
|
|
471
|
+
*
|
|
472
|
+
* `scaffold` is what the signed manifest declared: which archive, and the
|
|
473
|
+
* integrity it has to match. It is passed in rather than looked up because
|
|
474
|
+
* a scaffold that came from anywhere but the release under installation is
|
|
475
|
+
* not this release's scaffold.
|
|
476
|
+
*/
|
|
477
|
+
createBase(request: {
|
|
478
|
+
targetDir: string;
|
|
479
|
+
name: string;
|
|
480
|
+
scaffold?: KitScaffoldDescriptor;
|
|
481
|
+
}): Promise<void>;
|
|
482
|
+
}
|
|
483
|
+
interface ArtifactPort {
|
|
484
|
+
/** The bytes of one release artifact. Verified by digest before use. */
|
|
485
|
+
fetch(artifact: string): Promise<Uint8Array>;
|
|
486
|
+
}
|
|
487
|
+
/** Loads a module from the installed project graph, for tooling discovery. */
|
|
488
|
+
type ProjectModuleLoader = (specifier: string, projectDir: string) => Promise<unknown>;
|
|
489
|
+
interface KitAdapters {
|
|
490
|
+
clock: KitClock;
|
|
491
|
+
setupFetcher: SetupFetcher;
|
|
492
|
+
trustedKeys: readonly TrustedKey[];
|
|
493
|
+
catalog: CatalogPort;
|
|
494
|
+
license: LicensePort;
|
|
495
|
+
registry: RegistryPort;
|
|
496
|
+
credentials: KitCredentialStore;
|
|
497
|
+
packageManager: PackageManagerPort;
|
|
498
|
+
database: DatabasePort;
|
|
499
|
+
gates: GatePort;
|
|
500
|
+
git: GitPort;
|
|
501
|
+
scaffold: ScaffoldPort;
|
|
502
|
+
artifacts: ArtifactPort;
|
|
503
|
+
loadProjectModule: ProjectModuleLoader;
|
|
504
|
+
/** The running public CLI version, as the descriptor compares against. */
|
|
505
|
+
cliVersion: string;
|
|
506
|
+
controlPlaneUrl: string;
|
|
507
|
+
registryUrl: string;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Where a command gets its ports from.
|
|
512
|
+
*
|
|
513
|
+
* The live set — a real control-plane client, a real registry session, the real
|
|
514
|
+
* package manager — arrives with the work that connects `spfn kit` to the
|
|
515
|
+
* running service. Until then this build has no way to reach any of it, and it
|
|
516
|
+
* says so with a `CLI_`-prefixed code rather than pretending to be a Kit state:
|
|
517
|
+
* "this CLI cannot reach the control plane" is a fact about the CLI, not about
|
|
518
|
+
* the project's licence, lock or release.
|
|
519
|
+
*
|
|
520
|
+
* `setKitAdapterFactory` is the seam the tests drive the whole command surface
|
|
521
|
+
* through, with the fake world standing in for every external service.
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
interface AdapterRequest {
|
|
525
|
+
projectDir: string;
|
|
526
|
+
}
|
|
527
|
+
type KitAdapterFactory = (request: AdapterRequest) => Promise<KitAdapters>;
|
|
528
|
+
declare function setKitAdapterFactory(next: KitAdapterFactory | null): void;
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* The one place `spfn kit` talks to a network, and the rules that hold there.
|
|
532
|
+
*
|
|
533
|
+
* Three of them are worth stating, because every client below depends on all
|
|
534
|
+
* three and none of them can be added later:
|
|
535
|
+
*
|
|
536
|
+
* - a request body may hold a licence key or a local credential, so no error
|
|
537
|
+
* raised here ever quotes one. What a failure is allowed to carry is the
|
|
538
|
+
* method, the origin, the path and the status — never the body it sent and
|
|
539
|
+
* never the body it got back;
|
|
540
|
+
* - a response is a stranger's bytes. It is read with a ceiling on its size
|
|
541
|
+
* and a deadline on its arrival, so an origin that answers slowly or
|
|
542
|
+
* endlessly stops the command instead of hanging it;
|
|
543
|
+
* - "the service said no" and "the service could not be reached" are
|
|
544
|
+
* different answers. A transport failure becomes `CLI_...`, because it is a
|
|
545
|
+
* fact about this machine's connection rather than about the licence.
|
|
546
|
+
*/
|
|
547
|
+
|
|
548
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
549
|
+
interface KitHttpOptions {
|
|
550
|
+
/** Injected so tests reach a local fixture and nothing else. */
|
|
551
|
+
fetchImpl?: FetchLike;
|
|
552
|
+
timeoutMs?: number;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The real control-plane client: signed documents over HTTP, licence
|
|
557
|
+
* activation, and keeping this machine's local credential alive.
|
|
558
|
+
*
|
|
559
|
+
* Everything here is a translation layer and nothing here is a decision. The
|
|
560
|
+
* service answers in its own vocabulary — `LICENSE_REVOKED`,
|
|
561
|
+
* `PROJECT_LIMIT_REACHED`, `LOCAL_CREDENTIAL_STALE` — and the operations branch
|
|
562
|
+
* on the CLI's, so one table below maps one to the other. Where the two
|
|
563
|
+
* vocabularies do not line up the server's own code travels on in `detail`,
|
|
564
|
+
* because a report that says only "unavailable" when the server said
|
|
565
|
+
* `RATE_LIMITED` has thrown away the one fact that would have explained it.
|
|
566
|
+
*
|
|
567
|
+
* What this client never does is decide that a document is trustworthy. It
|
|
568
|
+
* returns the signed wrapper exactly as it arrived; the signature check lives
|
|
569
|
+
* in one place, and that place is not the transport.
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
interface ControlPlaneOptions extends KitHttpOptions {
|
|
573
|
+
/** Origin the licence routes are served from, e.g. `https://…`. */
|
|
574
|
+
baseUrl: string;
|
|
575
|
+
}
|
|
576
|
+
/** Signed catalogs and manifests, fetched and handed on unopened. */
|
|
577
|
+
declare class HttpCatalogPort implements CatalogPort {
|
|
578
|
+
private readonly http;
|
|
579
|
+
constructor(http?: KitHttpOptions);
|
|
580
|
+
fetchSignedCatalog(url: string): Promise<unknown>;
|
|
581
|
+
fetchSignedManifest(url: string): Promise<unknown>;
|
|
582
|
+
private fetchDocument;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Answers "does this licence still cover that release".
|
|
586
|
+
*
|
|
587
|
+
* The control plane has no endpoint for the question — entitlement is enforced
|
|
588
|
+
* where it is used, by the registry refusing to serve a version — so the live
|
|
589
|
+
* client answers it by asking the registry. The probe is injected rather than
|
|
590
|
+
* built in, because the licence client has no business knowing which package a
|
|
591
|
+
* Kit ships as.
|
|
592
|
+
*/
|
|
593
|
+
type EntitlementProbe = (request: EntitlementRequest) => Promise<EntitlementResult>;
|
|
594
|
+
declare class HttpLicensePort implements LicensePort {
|
|
595
|
+
private readonly baseUrl;
|
|
596
|
+
private readonly http;
|
|
597
|
+
private readonly probe;
|
|
598
|
+
constructor(options: ControlPlaneOptions, probe?: EntitlementProbe | null);
|
|
599
|
+
activate(request: ActivationRequest): Promise<ActivationResult>;
|
|
600
|
+
entitlement(request: EntitlementRequest): Promise<EntitlementResult>;
|
|
601
|
+
requestRecovery(request: {
|
|
602
|
+
activationId: string;
|
|
603
|
+
}): Promise<RecoveryRequestResult>;
|
|
604
|
+
completeRecovery(request: {
|
|
605
|
+
recoveryId: string;
|
|
606
|
+
challenge: string;
|
|
607
|
+
replacementCredential: string;
|
|
608
|
+
}): Promise<RecoveryCompletionResult>;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* The bearer the package manager installs with, and the rotation that keeps one
|
|
612
|
+
* available.
|
|
613
|
+
*
|
|
614
|
+
* There is no separate "session" to issue for a local install: the control
|
|
615
|
+
* plane accepts the machine's own local credential at the registry for as long
|
|
616
|
+
* as its access window is open, and issues a *new* credential when it is not.
|
|
617
|
+
* So this port's real job is the second half — notice a window about to close,
|
|
618
|
+
* rotate before it does, and put the replacement in the keychain in the same
|
|
619
|
+
* step, so a rotation that is not recorded can never have happened.
|
|
620
|
+
*/
|
|
621
|
+
declare class HttpRegistryPort implements RegistryPort {
|
|
622
|
+
private readonly baseUrl;
|
|
623
|
+
private readonly http;
|
|
624
|
+
private readonly credentials;
|
|
625
|
+
private readonly now;
|
|
626
|
+
constructor(options: ControlPlaneOptions & {
|
|
627
|
+
credentials: KitCredentialStore;
|
|
628
|
+
now: () => string;
|
|
629
|
+
});
|
|
630
|
+
issueSession(request: {
|
|
631
|
+
kitId?: string;
|
|
632
|
+
activationId: string;
|
|
633
|
+
localClientId: string;
|
|
634
|
+
credential: string;
|
|
635
|
+
forceRotation?: boolean;
|
|
636
|
+
}): Promise<RegistrySession>;
|
|
637
|
+
private rotate;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The private registry, reached through the licence proxy.
|
|
642
|
+
*
|
|
643
|
+
* The proxy is the place entitlement is actually enforced: it answers with the
|
|
644
|
+
* versions this activation may have and rewrites every tarball link to its own
|
|
645
|
+
* public origin, so "which releases do I have" is a question with a real answer
|
|
646
|
+
* even though the licence service has no endpoint for it.
|
|
647
|
+
*
|
|
648
|
+
* Integrity is checked twice on purpose, and the two checks catch different
|
|
649
|
+
* things. The first compares the version's `integrity` against the digest the
|
|
650
|
+
* *signed manifest* pinned: it catches a registry that serves a different
|
|
651
|
+
* package than the release was built from, and it happens before a byte is
|
|
652
|
+
* downloaded. The second hashes the bytes that actually arrived: it catches a
|
|
653
|
+
* tarball that was replaced in transit or in storage. Passing the first says
|
|
654
|
+
* the registry agrees with the manifest; only the second says the file on this
|
|
655
|
+
* disk is the file the manifest described.
|
|
656
|
+
*/
|
|
657
|
+
|
|
658
|
+
interface RegistryProxyOptions extends KitHttpOptions {
|
|
659
|
+
/** Public registry base, e.g. `https://…/npm/`. Trailing slash optional. */
|
|
660
|
+
registryUrl: string;
|
|
661
|
+
}
|
|
662
|
+
interface RegistryVersion {
|
|
663
|
+
version: string;
|
|
664
|
+
integrity: string;
|
|
665
|
+
tarball: string;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* What one metadata request produced.
|
|
669
|
+
*
|
|
670
|
+
* `status` separates the three answers that need different handling: the
|
|
671
|
+
* package was served, the credential was refused, or the proxy could not say.
|
|
672
|
+
* "Not entitled" and "no such package" are one answer (`not-found`) because the
|
|
673
|
+
* proxy deliberately makes them indistinguishable.
|
|
674
|
+
*/
|
|
675
|
+
interface RegistryMetadataResult {
|
|
676
|
+
status: 'ok' | 'not-found' | 'credential-rejected' | 'unavailable';
|
|
677
|
+
versions: RegistryVersion[];
|
|
678
|
+
detail?: string;
|
|
679
|
+
}
|
|
680
|
+
declare class KitRegistryProxyClient {
|
|
681
|
+
private readonly registryUrl;
|
|
682
|
+
private readonly http;
|
|
683
|
+
constructor(options: RegistryProxyOptions);
|
|
684
|
+
/** The entitled versions of one package, as the proxy reports them. */
|
|
685
|
+
metadata(packageName: string, credential: string): Promise<RegistryMetadataResult>;
|
|
686
|
+
/**
|
|
687
|
+
* The bytes of one exact version, proven against the manifest's digest.
|
|
688
|
+
*
|
|
689
|
+
* The tarball link comes from the proxy's own answer and is checked to stay
|
|
690
|
+
* on the registry origin: a rewritten link that points somewhere else is
|
|
691
|
+
* how an entitled request gets turned into a download from a stranger.
|
|
692
|
+
*/
|
|
693
|
+
fetchExact(request: {
|
|
694
|
+
packageName: string;
|
|
695
|
+
version: string;
|
|
696
|
+
integrity: string;
|
|
697
|
+
credential: string;
|
|
698
|
+
}): Promise<Uint8Array>;
|
|
699
|
+
private download;
|
|
700
|
+
}
|
|
701
|
+
/** Produces the bearer a release artifact is fetched with, or null. */
|
|
702
|
+
type ArtifactCredentialResolver = () => Promise<string | null>;
|
|
703
|
+
/**
|
|
704
|
+
* Release artifacts — managed files, agent packs, scaffold archives — fetched
|
|
705
|
+
* from the release store the catalog lives in.
|
|
706
|
+
*
|
|
707
|
+
* An artifact is paid content, so the store authenticates it exactly as the npm
|
|
708
|
+
* proxy does: the same bearer, the same refusal codes. That makes a refusal
|
|
709
|
+
* here a statement about *this machine's credential* rather than about the
|
|
710
|
+
* file, and the two send an agent to completely different next steps — only
|
|
711
|
+
* one of them is `spfn kit recover`.
|
|
712
|
+
*
|
|
713
|
+
* The setup descriptor, the catalog and the manifests stay public and are
|
|
714
|
+
* fetched without a bearer. They are locators and promises about a release;
|
|
715
|
+
* nothing anyone paid for is inside them.
|
|
716
|
+
*
|
|
717
|
+
* No digest is checked here, and that is not an omission: the caller holds the
|
|
718
|
+
* digest the *signed manifest* declared, and checking it anywhere else would
|
|
719
|
+
* mean a second place that decides what "correct bytes" means.
|
|
720
|
+
*/
|
|
721
|
+
declare class HttpArtifactPort implements ArtifactPort {
|
|
722
|
+
private readonly resolveBase;
|
|
723
|
+
private readonly http;
|
|
724
|
+
private readonly credential;
|
|
725
|
+
/**
|
|
726
|
+
* `baseUrl` may be a function because the release store is only known once
|
|
727
|
+
* the operation has a catalog URL — which is a fact about the release being
|
|
728
|
+
* installed, not about the machine installing it.
|
|
729
|
+
*/
|
|
730
|
+
constructor(options: KitHttpOptions & {
|
|
731
|
+
baseUrl: string | (() => string);
|
|
732
|
+
credential?: ArtifactCredentialResolver;
|
|
733
|
+
});
|
|
734
|
+
fetch(artifact: string): Promise<Uint8Array>;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Proving the exact graph before installing it.
|
|
739
|
+
*
|
|
740
|
+
* A frozen install is only as exact as the registry it pulls from. The lockfile
|
|
741
|
+
* pins versions, the manifest pins digests, and between them sits a proxy that
|
|
742
|
+
* could serve a different tarball under the same name — so every package the
|
|
743
|
+
* signed manifest names is fetched through the proxy and checked against the
|
|
744
|
+
* manifest's own integrity *before* the package manager is allowed to run.
|
|
745
|
+
*
|
|
746
|
+
* This is a decorator rather than a package manager: what actually installs is
|
|
747
|
+
* still pnpm, npm or whatever the project uses. The verification wraps it,
|
|
748
|
+
* because "which packages must be exact" is a release fact and "how do I put
|
|
749
|
+
* them on disk" is not.
|
|
750
|
+
*
|
|
751
|
+
* The manifest is not passed in. It is read from the operation context the
|
|
752
|
+
* running operation already wrote, and re-verified against the trusted keys
|
|
753
|
+
* here — a manifest trusted because a caller handed it over is a manifest
|
|
754
|
+
* trusted for the wrong reason.
|
|
755
|
+
*/
|
|
756
|
+
|
|
757
|
+
interface RegistryVerifyingPackageManagerOptions {
|
|
758
|
+
catalog: CatalogPort;
|
|
759
|
+
trustedKeys: readonly TrustedKey[];
|
|
760
|
+
registry: KitRegistryProxyClient;
|
|
761
|
+
/** What performs the install once the graph is proven. */
|
|
762
|
+
delegate: PackageManagerPort;
|
|
763
|
+
/** Package names to skip, e.g. ones the private registry does not carry. */
|
|
764
|
+
skip?: (name: string) => boolean;
|
|
765
|
+
}
|
|
766
|
+
declare class RegistryVerifyingPackageManager implements PackageManagerPort {
|
|
767
|
+
private readonly options;
|
|
768
|
+
constructor(options: RegistryVerifyingPackageManagerOptions);
|
|
769
|
+
install(request: {
|
|
770
|
+
cwd: string;
|
|
771
|
+
frozen: boolean;
|
|
772
|
+
env: Record<string, string>;
|
|
773
|
+
}): Promise<PackageInstallResult>;
|
|
774
|
+
private verifyGraph;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Assembling the half of the Kit world that lives on the network.
|
|
779
|
+
*
|
|
780
|
+
* Six ports of the ten are remote or artifact-shaped — catalog, licence,
|
|
781
|
+
* registry, artifacts, scaffold and the verification in front of the package
|
|
782
|
+
* manager — and this builds all six from three addresses. The other four
|
|
783
|
+
* (package manager, database, gates, Git) are local process work and are handed
|
|
784
|
+
* in, because how a project runs its own tests is not something a control-plane
|
|
785
|
+
* client should have an opinion about.
|
|
786
|
+
*
|
|
787
|
+
* The addresses are resolved per project, in one order and no other: what the
|
|
788
|
+
* checkout already recorded when it was activated, then what the environment
|
|
789
|
+
* overrides for a staging or local run, then the published defaults. A checkout
|
|
790
|
+
* activated against one control plane must not silently start talking to
|
|
791
|
+
* another because a shell variable was left set.
|
|
792
|
+
*/
|
|
793
|
+
|
|
794
|
+
/** Where a Kit's services live when nothing says otherwise. */
|
|
795
|
+
declare const DEFAULT_CONTROL_PLANE_URL = "https://start.superfunction.xyz";
|
|
796
|
+
declare const DEFAULT_REGISTRY_URL = "https://packages.superfunction.xyz/npm/";
|
|
797
|
+
/** Environment overrides, for a staging control plane or a local server. */
|
|
798
|
+
declare const CONTROL_PLANE_URL_ENV = "SPFN_KIT_CONTROL_PLANE_URL";
|
|
799
|
+
declare const REGISTRY_URL_ENV = "SPFN_KIT_REGISTRY_URL";
|
|
800
|
+
interface KitEndpoints {
|
|
801
|
+
controlPlaneUrl: string;
|
|
802
|
+
registryUrl: string;
|
|
803
|
+
/** Where the addresses came from, for the status report. */
|
|
804
|
+
source: 'checkout' | 'environment' | 'default';
|
|
805
|
+
}
|
|
806
|
+
declare function resolveKitEndpoints(projectDir: string, env?: NodeJS.ProcessEnv): KitEndpoints;
|
|
807
|
+
interface KitRemotePortsOptions extends KitHttpOptions {
|
|
808
|
+
projectDir: string;
|
|
809
|
+
endpoints: KitEndpoints;
|
|
810
|
+
credentials: KitCredentialStore;
|
|
811
|
+
trustedKeys: readonly TrustedKey[];
|
|
812
|
+
now: () => string;
|
|
813
|
+
/** What actually installs, once the graph has been proven exact. */
|
|
814
|
+
packageManager: PackageManagerPort;
|
|
815
|
+
/** Packages the private registry does not serve, e.g. public ones. */
|
|
816
|
+
skipVerification?: (name: string) => boolean;
|
|
817
|
+
}
|
|
818
|
+
interface KitRemotePorts {
|
|
819
|
+
/** Fetches the setup descriptor, one allowlist-checked hop at a time. */
|
|
820
|
+
setupFetcher: SetupFetcher;
|
|
821
|
+
catalog: HttpCatalogPort;
|
|
822
|
+
license: HttpLicensePort;
|
|
823
|
+
registry: HttpRegistryPort;
|
|
824
|
+
artifacts: HttpArtifactPort;
|
|
825
|
+
scaffold: ArtifactScaffoldPort;
|
|
826
|
+
packageManager: RegistryVerifyingPackageManager;
|
|
827
|
+
/** Exposed so a caller can ask the registry a question directly. */
|
|
828
|
+
registryClient: KitRegistryProxyClient;
|
|
829
|
+
}
|
|
830
|
+
declare function createKitRemotePorts(options: KitRemotePortsOptions): KitRemotePorts;
|
|
831
|
+
/**
|
|
832
|
+
* A last-resort guess at the package a Kit ships as.
|
|
833
|
+
*
|
|
834
|
+
* Only for a caller with no manifest in hand. The convention it encodes —
|
|
835
|
+
* one scoped package named after the Kit — is not true of every Kit:
|
|
836
|
+
* `campaign-landing` ships as `@superfunction/landing-kit`, and asking the
|
|
837
|
+
* registry about `@superfunction/campaign-landing` gets a 404 that reads as
|
|
838
|
+
* "your licence does not cover this release". Anything that holds a manifest
|
|
839
|
+
* passes `packageName` instead of relying on this.
|
|
840
|
+
*/
|
|
841
|
+
declare function kitPackageName(kitId: string): string;
|
|
842
|
+
|
|
843
|
+
interface RunRequest {
|
|
844
|
+
file: string;
|
|
845
|
+
args: string[];
|
|
846
|
+
cwd: string;
|
|
847
|
+
/** Secret-free variables the child needs beyond the passthrough set. */
|
|
848
|
+
extraEnv?: Record<string, string>;
|
|
849
|
+
/** The one secret a child may receive, and only through its environment. */
|
|
850
|
+
registryToken?: string;
|
|
851
|
+
/** Parent variable names this child may also read. Never secrets. */
|
|
852
|
+
passthrough?: readonly string[];
|
|
853
|
+
timeoutMs?: number;
|
|
854
|
+
}
|
|
855
|
+
interface RunResult {
|
|
856
|
+
exitCode: number;
|
|
857
|
+
stdout: string;
|
|
858
|
+
stderr: string;
|
|
859
|
+
/** True when the binary was not there at all, rather than exiting badly. */
|
|
860
|
+
missing: boolean;
|
|
861
|
+
}
|
|
862
|
+
type CommandRunner = (request: RunRequest) => Promise<RunResult>;
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* The package manager, actually run.
|
|
866
|
+
*
|
|
867
|
+
* "Frozen" is the whole point of this port. A Kit release pins an exact graph
|
|
868
|
+
* in a lockfile, and an install that quietly resolves something newer has
|
|
869
|
+
* produced a project the release never described — so the frozen flag is
|
|
870
|
+
* passed through to pnpm as `--frozen-lockfile`, and a lockfile that no longer
|
|
871
|
+
* matches `package.json` fails the install rather than being rewritten.
|
|
872
|
+
*
|
|
873
|
+
* The registry session never becomes an argument and never becomes a file. It
|
|
874
|
+
* reaches the child as npm configuration in that child's own environment,
|
|
875
|
+
* addressed to the registry it is good for — so the token is in the process's
|
|
876
|
+
* own memory and in no argument list, no `.npmrc` and no log. The committed
|
|
877
|
+
* `.npmrc` only maps the release's scopes to the registry, which is public.
|
|
878
|
+
*
|
|
879
|
+
* Why a failure is classified at all: unit 06 lets exactly one failure be
|
|
880
|
+
* retried with a fresh session, and only one. Everything else would fail
|
|
881
|
+
* identically the second time, so telling "the registry refused us" apart from
|
|
882
|
+
* "this graph cannot be resolved" is what keeps the retry meaningful instead of
|
|
883
|
+
* doubling every genuine failure.
|
|
884
|
+
*/
|
|
885
|
+
|
|
886
|
+
interface PnpmPackageManagerOptions {
|
|
887
|
+
/** Registry the install resolves against. Public URL, never a secret. */
|
|
888
|
+
registryUrl?: string;
|
|
889
|
+
/**
|
|
890
|
+
* Secret-free variables the install needs, e.g. `npm_config_userconfig`
|
|
891
|
+
* pointed at an empty file so a machine's own `~/.npmrc` cannot redirect a
|
|
892
|
+
* scope somewhere the release never named. The operation's own environment
|
|
893
|
+
* takes precedence over anything here.
|
|
894
|
+
*/
|
|
895
|
+
extraEnv?: Record<string, string>;
|
|
896
|
+
/** A store outside the user's own, for a run that must not touch it. */
|
|
897
|
+
storeDir?: string;
|
|
898
|
+
/** Refuse to run a dependency's install scripts. */
|
|
899
|
+
ignoreScripts?: boolean;
|
|
900
|
+
/** Treat the project as standalone, whatever it happens to sit inside. */
|
|
901
|
+
ignoreWorkspace?: boolean;
|
|
902
|
+
timeoutMs?: number;
|
|
903
|
+
/** Injected so tests drive the classification without spawning pnpm. */
|
|
904
|
+
run?: CommandRunner;
|
|
905
|
+
/** The binary to run. `pnpm` unless a test or a project says otherwise. */
|
|
906
|
+
binary?: string;
|
|
907
|
+
}
|
|
908
|
+
declare class PnpmPackageManagerPort implements PackageManagerPort {
|
|
909
|
+
private readonly options;
|
|
910
|
+
constructor(options?: PnpmPackageManagerOptions);
|
|
911
|
+
install(request: {
|
|
912
|
+
cwd: string;
|
|
913
|
+
frozen: boolean;
|
|
914
|
+
env: Record<string, string>;
|
|
915
|
+
}): Promise<PackageInstallResult>;
|
|
916
|
+
/**
|
|
917
|
+
* The arguments, with the registry among them and the session not.
|
|
918
|
+
*
|
|
919
|
+
* A registry URL is a public locator, so it is safe on a command line; the
|
|
920
|
+
* bearer that opens it is not, and travels in the environment instead.
|
|
921
|
+
*/
|
|
922
|
+
private argumentsFor;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* The project's database, asked through the project's own CLI.
|
|
927
|
+
*
|
|
928
|
+
* The asking is done by a child process rather than in this one, and that is
|
|
929
|
+
* the design rather than an accident. Reading a project's migration state
|
|
930
|
+
* means loading that project's environment, and loading it in *this* process
|
|
931
|
+
* would pull the project's secrets — a connection string with a password in it
|
|
932
|
+
* — into the memory of a command that has no use for them and prints reports
|
|
933
|
+
* for a living. A child loads them, answers one question, and exits.
|
|
934
|
+
*
|
|
935
|
+
* The question is asked in the machine-readable form `spfn db status --json`
|
|
936
|
+
* exists for. Nothing here parses prose: a human summary is written to change
|
|
937
|
+
* when it reads better, and a port that depended on its wording would break
|
|
938
|
+
* quietly the first time someone improved it.
|
|
939
|
+
*/
|
|
940
|
+
|
|
941
|
+
interface SpfnDatabaseOptions {
|
|
942
|
+
/** How to run this CLI. Defaults to the binary this build ships with. */
|
|
943
|
+
command?: {
|
|
944
|
+
file: string;
|
|
945
|
+
args: string[];
|
|
946
|
+
};
|
|
947
|
+
timeoutMs?: number;
|
|
948
|
+
run?: CommandRunner;
|
|
949
|
+
}
|
|
950
|
+
declare class SpfnDatabasePort implements DatabasePort {
|
|
951
|
+
private readonly options;
|
|
952
|
+
constructor(options?: SpfnDatabaseOptions);
|
|
953
|
+
status(request: {
|
|
954
|
+
cwd: string;
|
|
955
|
+
}): Promise<DatabaseStatus>;
|
|
956
|
+
/**
|
|
957
|
+
* Apply what is waiting, then look again.
|
|
958
|
+
*
|
|
959
|
+
* The second look is not belt and braces: `spfn db migrate` reports what it
|
|
960
|
+
* attempted, and this port has to report what is *true afterwards*. A
|
|
961
|
+
* migration that exits 0 having applied three of four is a failure, and
|
|
962
|
+
* only re-reading the status says so.
|
|
963
|
+
*/
|
|
964
|
+
migrate(request: {
|
|
965
|
+
cwd: string;
|
|
966
|
+
withBackup: boolean;
|
|
967
|
+
}): Promise<MigrationResult>;
|
|
968
|
+
private report;
|
|
969
|
+
private run;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* The release's gates, run where the release lives.
|
|
974
|
+
*
|
|
975
|
+
* A gate is the release's own claim that the project it just produced works:
|
|
976
|
+
* it typechecks, its tests pass, it builds. So each one runs the project's own
|
|
977
|
+
* script wherever the project has one, and falls back to the obvious command
|
|
978
|
+
* only where there is an obvious one. A gate a project has no way to run is a
|
|
979
|
+
* failure with a reason, not a silent pass — "we could not check" and "we
|
|
980
|
+
* checked and it is fine" are different claims, and only one of them may let
|
|
981
|
+
* an install commit.
|
|
982
|
+
*
|
|
983
|
+
* `db-status` is not a command at all. It is the database port's own answer,
|
|
984
|
+
* asked again, because a gate that shelled out to a second implementation of
|
|
985
|
+
* the same question could disagree with the migration step it follows.
|
|
986
|
+
*
|
|
987
|
+
* `health` never reaches here: it is a deployment gate, and the operation
|
|
988
|
+
* filters it out before local gates run.
|
|
989
|
+
*/
|
|
990
|
+
|
|
991
|
+
interface CommandGateOptions {
|
|
992
|
+
/** Answers the `db-status` gate. The same port the migration step used. */
|
|
993
|
+
database: DatabasePort;
|
|
994
|
+
packageManagerBinary?: string;
|
|
995
|
+
timeoutMs?: number;
|
|
996
|
+
run?: CommandRunner;
|
|
997
|
+
}
|
|
998
|
+
declare class CommandGatePort implements GatePort {
|
|
999
|
+
private readonly options;
|
|
1000
|
+
constructor(options: CommandGateOptions);
|
|
1001
|
+
run(gate: KitGate, request: {
|
|
1002
|
+
cwd: string;
|
|
1003
|
+
}): Promise<GateResult>;
|
|
1004
|
+
private commandFor;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Git, in the narrow shape unit 06 asks for: start a repository, say whether
|
|
1009
|
+
* it is clean, make one commit, name the current one.
|
|
1010
|
+
*
|
|
1011
|
+
* Deliberately nothing else. No remote is added, nothing is pushed, no branch
|
|
1012
|
+
* is renamed and no history is rewritten — a Kit install finishes at a
|
|
1013
|
+
* verified local repository, and a port that could push would make that
|
|
1014
|
+
* boundary a matter of trust rather than of capability.
|
|
1015
|
+
*
|
|
1016
|
+
* The identity question is handled the way it should be: the user's own
|
|
1017
|
+
* `user.name` and `user.email` are used whenever Git can find them, and a
|
|
1018
|
+
* fallback is supplied *for one command* only when it cannot. Writing an
|
|
1019
|
+
* identity into the repository's config would change a setting the person
|
|
1020
|
+
* owns; passing it with `-c` changes nothing that outlives the commit.
|
|
1021
|
+
*/
|
|
1022
|
+
|
|
1023
|
+
interface SystemGitOptions {
|
|
1024
|
+
binary?: string;
|
|
1025
|
+
timeoutMs?: number;
|
|
1026
|
+
run?: CommandRunner;
|
|
1027
|
+
}
|
|
1028
|
+
declare class SystemGitPort implements GitPort {
|
|
1029
|
+
private readonly options;
|
|
1030
|
+
constructor(options?: SystemGitOptions);
|
|
1031
|
+
/** Start a repository, or leave the one that is already there alone. */
|
|
1032
|
+
init(request: {
|
|
1033
|
+
cwd: string;
|
|
1034
|
+
}): Promise<void>;
|
|
1035
|
+
/**
|
|
1036
|
+
* Whether the worktree has nothing uncommitted.
|
|
1037
|
+
*
|
|
1038
|
+
* A Git call that fails is *not* reported as clean. An update refuses to
|
|
1039
|
+
* start on a dirty worktree, and answering "clean" because the question
|
|
1040
|
+
* could not be asked would turn a missing answer into permission.
|
|
1041
|
+
*/
|
|
1042
|
+
isClean(request: {
|
|
1043
|
+
cwd: string;
|
|
1044
|
+
}): Promise<boolean>;
|
|
1045
|
+
/**
|
|
1046
|
+
* Commit whatever the operation produced, or report the commit already
|
|
1047
|
+
* there when it produced nothing to record.
|
|
1048
|
+
*
|
|
1049
|
+
* "Nothing to commit" is not a failure. A resume reaches this after the
|
|
1050
|
+
* previous run committed, and an operation whose whole effect was ignored
|
|
1051
|
+
* by `.gitignore` reaches it too — in neither case is refusing with a raw
|
|
1052
|
+
* error the right answer, and the second case is caught where it belongs,
|
|
1053
|
+
* by the check that the committed state is actually tracked.
|
|
1054
|
+
*/
|
|
1055
|
+
commit(request: {
|
|
1056
|
+
cwd: string;
|
|
1057
|
+
message: string;
|
|
1058
|
+
}): Promise<{
|
|
1059
|
+
commit: string;
|
|
1060
|
+
}>;
|
|
1061
|
+
trackedAmong(request: {
|
|
1062
|
+
cwd: string;
|
|
1063
|
+
paths: readonly string[];
|
|
1064
|
+
}): Promise<string[]>;
|
|
1065
|
+
/** The current commit, or null in a repository that has none yet. */
|
|
1066
|
+
head(request: {
|
|
1067
|
+
cwd: string;
|
|
1068
|
+
}): Promise<string | null>;
|
|
1069
|
+
/**
|
|
1070
|
+
* `-c user.name=… -c user.email=…`, but only when Git has neither.
|
|
1071
|
+
*
|
|
1072
|
+
* `--no-verify` above and this together mean a commit works on a machine
|
|
1073
|
+
* that has never been set up, without either one changing what the machine
|
|
1074
|
+
* does for anything else.
|
|
1075
|
+
*/
|
|
1076
|
+
private identityArguments;
|
|
1077
|
+
private git;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* The four ports that are local process work, assembled.
|
|
1082
|
+
*
|
|
1083
|
+
* Everything a Kit operation does on this machine rather than over the network
|
|
1084
|
+
* ends up here: installing the graph, applying migrations, running the
|
|
1085
|
+
* release's gates and making the first commit. They are built together because
|
|
1086
|
+
* two of them depend on a third — the `db-status` gate has to be the same
|
|
1087
|
+
* database port the migration step used, or a gate could contradict the step
|
|
1088
|
+
* it follows.
|
|
1089
|
+
*/
|
|
1090
|
+
|
|
1091
|
+
interface KitLocalPortsOptions {
|
|
1092
|
+
packageManager?: PnpmPackageManagerOptions;
|
|
1093
|
+
database?: SpfnDatabaseOptions;
|
|
1094
|
+
git?: SystemGitOptions;
|
|
1095
|
+
gateTimeoutMs?: number;
|
|
1096
|
+
}
|
|
1097
|
+
interface KitLocalPorts {
|
|
1098
|
+
packageManager: PackageManagerPort;
|
|
1099
|
+
database: DatabasePort;
|
|
1100
|
+
gates: GatePort;
|
|
1101
|
+
git: GitPort;
|
|
1102
|
+
loadProjectModule: ProjectModuleLoader;
|
|
1103
|
+
}
|
|
1104
|
+
declare function createKitLocalPorts(options?: KitLocalPortsOptions): KitLocalPorts;
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* All ten ports, for a real run.
|
|
1108
|
+
*
|
|
1109
|
+
* This is the join: the six that reach the network or a release artifact, and
|
|
1110
|
+
* the four that run something on this machine. Nothing here decides policy —
|
|
1111
|
+
* every refusal, every retry and every checkpoint still lives in the
|
|
1112
|
+
* operations. What this file decides is only *which* implementation answers,
|
|
1113
|
+
* and it makes three choices worth naming:
|
|
1114
|
+
*
|
|
1115
|
+
* - the credential store is the real keychain, under the Kit's own service.
|
|
1116
|
+
* A Kit credential is never reachable through the env-secret path, and the
|
|
1117
|
+
* separation is a constructor argument rather than a convention;
|
|
1118
|
+
* - the trusted keys are the ones built into this build. Nothing about a
|
|
1119
|
+
* project can widen them;
|
|
1120
|
+
* - the addresses come from the checkout first. A project activated against
|
|
1121
|
+
* one control plane keeps talking to that one.
|
|
1122
|
+
*
|
|
1123
|
+
* `spfn kit status` and `spfn kit check` reach this too, and must not be
|
|
1124
|
+
* stopped by it: they are read-only and have to work on a machine with no
|
|
1125
|
+
* keychain and no network. So nothing here connects, opens or checks anything
|
|
1126
|
+
* at construction time — every port is inert until an operation calls it.
|
|
1127
|
+
*/
|
|
1128
|
+
|
|
1129
|
+
interface LiveKitAdapterOptions extends KitLocalPortsOptions {
|
|
1130
|
+
/** Overridden by tests and by an integration run against a fixture. */
|
|
1131
|
+
endpoints?: KitEndpoints;
|
|
1132
|
+
fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
1133
|
+
timeoutMs?: number;
|
|
1134
|
+
env?: NodeJS.ProcessEnv;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* The adapters `spfn kit` uses when nothing has replaced them.
|
|
1138
|
+
*
|
|
1139
|
+
* An ISO instant with second precision, not milliseconds: the contract's
|
|
1140
|
+
* timestamps are second-precision, and an operation ID derived from a
|
|
1141
|
+
* millisecond clock would not match the pattern the journal is checked against.
|
|
1142
|
+
*/
|
|
1143
|
+
declare function createLiveKitAdapters(request: AdapterRequest, options?: LiveKitAdapterOptions): KitAdapters;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* The public keys this CLI will accept a signed Kit document from.
|
|
1147
|
+
*
|
|
1148
|
+
* The list is built in rather than fetched, and that is the point: a trust root
|
|
1149
|
+
* that arrives over the network is not a trust root, because whoever can serve
|
|
1150
|
+
* the document can serve the key that vouches for it. Keys change by shipping a
|
|
1151
|
+
* new CLI, which is a thing customers can see happen.
|
|
1152
|
+
*
|
|
1153
|
+
* The list is empty in this build. The release signing key is not published
|
|
1154
|
+
* yet, so every signed document fails verification, and that is the correct
|
|
1155
|
+
* behaviour for a CLI that has no way to tell a real release from a forged one
|
|
1156
|
+
* — it fails closed, with `KIT_MANIFEST_INVALID`, rather than installing
|
|
1157
|
+
* whatever it was handed.
|
|
1158
|
+
*
|
|
1159
|
+
* `SPFN_KIT_TRUSTED_KEYS` exists for the runs that legitimately verify against
|
|
1160
|
+
* a key that is not the published one: an integration against a staging
|
|
1161
|
+
* control plane, or a release rehearsal. It *replaces* the built-in list rather
|
|
1162
|
+
* than adding to it, so a stray variable can only ever narrow what this CLI
|
|
1163
|
+
* trusts down to keys the person running it chose.
|
|
1164
|
+
*/
|
|
1165
|
+
|
|
1166
|
+
/** Keys shipped with this build. Empty until the signing key is published. */
|
|
1167
|
+
declare const BUILT_IN_TRUSTED_KEYS: readonly TrustedKey[];
|
|
1168
|
+
declare const TRUSTED_KEYS_ENV = "SPFN_KIT_TRUSTED_KEYS";
|
|
1169
|
+
/**
|
|
1170
|
+
* The keys to verify with, and where they came from.
|
|
1171
|
+
*
|
|
1172
|
+
* A malformed override is rejected outright rather than partially honoured: a
|
|
1173
|
+
* trust list that silently dropped the entry someone meant to add would verify
|
|
1174
|
+
* with whatever was left, which is the one outcome worth avoiding here.
|
|
1175
|
+
*/
|
|
1176
|
+
declare function resolveTrustedKeys(env?: NodeJS.ProcessEnv): readonly TrustedKey[];
|
|
1177
|
+
|
|
120
1178
|
declare function run(): Promise<void>;
|
|
121
1179
|
|
|
122
|
-
export { type CustomDomains, type DeploymentConfig, type EnvironmentVariables, type PackageManager, type Region, type SpfnConfig, run };
|
|
1180
|
+
export { BUILT_IN_TRUSTED_KEYS, CONTROL_PLANE_URL_ENV, CommandGatePort, type CustomDomains, DEFAULT_CONTROL_PLANE_URL, DEFAULT_REGISTRY_URL, type DeploymentConfig, type EnvironmentVariables, type KitAdapterFactory, type KitAdapters, type KitEndpoints, type KitLocalPorts, type KitLocalPortsOptions, type KitRemotePorts, type KitRemotePortsOptions, type LiveKitAdapterOptions, type PackageManager, PnpmPackageManagerPort, REGISTRY_URL_ENV, type Region, type SpfnConfig, SpfnDatabasePort, SystemGitPort, TRUSTED_KEYS_ENV, createKitLocalPorts, createKitRemotePorts, createLiveKitAdapters, kitPackageName, resolveKitEndpoints, resolveTrustedKeys, run, setKitAdapterFactory };
|