@kedge/server 0.9.0 → 0.10.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/package.json +6 -3
- package/src/access.ts +5 -0
- package/src/admin-routes.ts +79 -3
- package/src/entity-routes.ts +20 -16
- package/src/types.ts +32 -0
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kedge/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/chandeldivyam/Kedge.git",
|
|
8
8
|
"directory": "packages/server"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24"
|
|
12
|
+
},
|
|
10
13
|
"type": "module",
|
|
11
14
|
"description": "Hono app: /store, /admin, /health.",
|
|
12
15
|
"exports": {
|
|
@@ -19,8 +22,8 @@
|
|
|
19
22
|
"hono": "4.13.13",
|
|
20
23
|
"@hono/zod-openapi": "1.6.3",
|
|
21
24
|
"zod": "4.6.7",
|
|
22
|
-
"@kedge/core": "0.
|
|
23
|
-
"@kedge/plugin-sdk": "0.
|
|
25
|
+
"@kedge/core": "0.10.0",
|
|
26
|
+
"@kedge/plugin-sdk": "0.10.0"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@cloudflare/workers-types": "5.20261024.1"
|
package/src/access.ts
CHANGED
|
@@ -307,6 +307,11 @@ export function adminGate(auth: AuthDeps, perms: AdminPermissions, now: () => nu
|
|
|
307
307
|
if (cred?.via === "bearer" && cred.token.startsWith("sk_")) {
|
|
308
308
|
const found = await resolveKey(c, auth, cred.token, t);
|
|
309
309
|
access = { ctx: found.ctx, via: "key", key: found.key, resolved: found };
|
|
310
|
+
} else if (cred?.via === "bearer" && cred.token.startsWith("kcs_")) {
|
|
311
|
+
// A CLI session (p0.10 §6.2): admitted here, on /admin/* only, never as a staff session.
|
|
312
|
+
const found = await auth.access.cliSession(cred.token, t);
|
|
313
|
+
if (!found) throw new KedgeError("UNAUTHORIZED", "This CLI session has ended; run the command again");
|
|
314
|
+
access = { ctx: found.ctx, via: "bearer", resolved: found };
|
|
310
315
|
} else if (cred) {
|
|
311
316
|
access = await resolveSession(c, auth, "staff", cred, t);
|
|
312
317
|
} else {
|
package/src/admin-routes.ts
CHANGED
|
@@ -485,14 +485,18 @@ export function adminRoutes(
|
|
|
485
485
|
createRoute({
|
|
486
486
|
method: "put",
|
|
487
487
|
path: "/admin/storefronts/{id}/origins",
|
|
488
|
-
request: {
|
|
488
|
+
request: {
|
|
489
|
+
params: z.object({ id: ID }),
|
|
490
|
+
// `expected`: the list as the caller read it; the write is refused CONFLICT if it changed since.
|
|
491
|
+
...jsonBody(z.object({ allowed_origins: Origins, expected: Origins.optional() })),
|
|
492
|
+
},
|
|
489
493
|
responses: ok("The storefront's origins"),
|
|
490
494
|
}),
|
|
491
495
|
),
|
|
492
496
|
async (c) => {
|
|
493
497
|
const { id } = c.req.valid("param");
|
|
494
|
-
const { allowed_origins } = c.req.valid("json");
|
|
495
|
-
await admin.setOrigins(ctx(c), id, allowed_origins, req(c));
|
|
498
|
+
const { allowed_origins, expected } = c.req.valid("json");
|
|
499
|
+
await admin.setOrigins(ctx(c), id, allowed_origins, req(c), expected);
|
|
496
500
|
return c.json({ id, allowed_origins }, 200);
|
|
497
501
|
},
|
|
498
502
|
);
|
|
@@ -575,6 +579,78 @@ export function adminRoutes(
|
|
|
575
579
|
},
|
|
576
580
|
);
|
|
577
581
|
|
|
582
|
+
// ---- credential references (p0.10 §6.2) ----------------------------------------------
|
|
583
|
+
// The permission depends on the ref's kind (api_key:write for a secret key,
|
|
584
|
+
// storefront:write otherwise), so the store authorizes inside its own batch.
|
|
585
|
+
|
|
586
|
+
const Ref = z.object({ ref: z.string().regex(/^[A-Za-z0-9_-]{32,128}$/) });
|
|
587
|
+
const RefKind = z.enum(["secret", "publishable", "storefront"]);
|
|
588
|
+
const refJson = (r: {
|
|
589
|
+
ref: string;
|
|
590
|
+
kind: string;
|
|
591
|
+
state: string;
|
|
592
|
+
storefrontId: string | null;
|
|
593
|
+
keyId: string | null;
|
|
594
|
+
}) => ({ ref: r.ref, kind: r.kind, state: r.state, storefront_id: r.storefrontId, key_id: r.keyId });
|
|
595
|
+
app.openapi(
|
|
596
|
+
perms.declare(
|
|
597
|
+
{ permission: "api_key:write", store: true },
|
|
598
|
+
createRoute({
|
|
599
|
+
method: "put",
|
|
600
|
+
path: "/admin/credential-refs/{ref}",
|
|
601
|
+
request: {
|
|
602
|
+
params: Ref,
|
|
603
|
+
...jsonBody(
|
|
604
|
+
z.discriminatedUnion("kind", [
|
|
605
|
+
z.object({
|
|
606
|
+
kind: z.literal("secret"),
|
|
607
|
+
name: z.string().min(1).max(80),
|
|
608
|
+
grants: Grants.min(1),
|
|
609
|
+
location_ids: z.array(ID).max(64).optional(),
|
|
610
|
+
}),
|
|
611
|
+
z.object({ kind: z.literal("publishable"), name: z.string().min(1).max(80), storefront_id: ID }),
|
|
612
|
+
z.object({ kind: z.literal("storefront"), name: z.string().min(1).max(80), allowed_origins: Origins }),
|
|
613
|
+
]),
|
|
614
|
+
),
|
|
615
|
+
},
|
|
616
|
+
responses: ok("The key the ref made, shown once; CREDENTIAL_REF_TAKEN if it was used or voided"),
|
|
617
|
+
}),
|
|
618
|
+
),
|
|
619
|
+
async (c) => {
|
|
620
|
+
once(c);
|
|
621
|
+
const b = c.req.valid("json");
|
|
622
|
+
const made = await admin.createByRef(
|
|
623
|
+
ctx(c),
|
|
624
|
+
c.req.valid("param").ref,
|
|
625
|
+
b.kind === "secret"
|
|
626
|
+
? {
|
|
627
|
+
kind: "secret",
|
|
628
|
+
name: b.name,
|
|
629
|
+
grants: grants(b.grants),
|
|
630
|
+
...(b.location_ids ? { locationIds: b.location_ids } : {}),
|
|
631
|
+
}
|
|
632
|
+
: b.kind === "publishable"
|
|
633
|
+
? { kind: "publishable", name: b.name, storefrontId: b.storefront_id }
|
|
634
|
+
: { kind: "storefront", name: b.name, allowedOrigins: b.allowed_origins },
|
|
635
|
+
req(c),
|
|
636
|
+
);
|
|
637
|
+
return c.json({ ...refJson(made), key: made.key, token_prefix: made.tokenPrefix }, 200);
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
app.openapi(
|
|
641
|
+
perms.declare(
|
|
642
|
+
{ permission: "api_key:write", store: true },
|
|
643
|
+
createRoute({
|
|
644
|
+
method: "post",
|
|
645
|
+
path: "/admin/credential-refs/{ref}/void",
|
|
646
|
+
request: { params: Ref, ...jsonBody(z.object({ kind: RefKind })) },
|
|
647
|
+
responses: ok("The ref's outcome, kept for ever: fenced, or revoked with what it made"),
|
|
648
|
+
}),
|
|
649
|
+
),
|
|
650
|
+
async (c) =>
|
|
651
|
+
c.json(refJson(await admin.voidRef(ctx(c), c.req.valid("param").ref, c.req.valid("json").kind, req(c))), 200),
|
|
652
|
+
);
|
|
653
|
+
|
|
578
654
|
// ---- API keys ----------------------------------------------------------------------
|
|
579
655
|
|
|
580
656
|
const KeyParam = z.object({ id: ID });
|
package/src/entity-routes.ts
CHANGED
|
@@ -184,22 +184,26 @@ export function entityRoutes(
|
|
|
184
184
|
method: "post",
|
|
185
185
|
path: `${base}/search`,
|
|
186
186
|
request: jsonBody(
|
|
187
|
-
z
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
187
|
+
z
|
|
188
|
+
.object({
|
|
189
|
+
filters: z
|
|
190
|
+
.array(z.object({ field: z.string(), op: z.enum(["eq", "in", "gte", "lt"]), value: z.unknown() }))
|
|
191
|
+
.max(16)
|
|
192
|
+
.optional(),
|
|
193
|
+
search: z
|
|
194
|
+
.object({
|
|
195
|
+
fields: z.array(z.string()).min(1).max(ENTITY_LIMITS.SEARCH_FIELDS_MAX),
|
|
196
|
+
term: z.string().min(1).max(200),
|
|
197
|
+
})
|
|
198
|
+
.optional(),
|
|
199
|
+
sort: z.string().max(64).optional(),
|
|
200
|
+
state: z.enum(["live", "archived"]).optional(),
|
|
201
|
+
cursor: z.string().max(2048).optional(),
|
|
202
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
203
|
+
})
|
|
204
|
+
// A key it doesn't know is refused, naming it, never ignored: a mistyped `filter`
|
|
205
|
+
// would otherwise answer every row as if it had narrowed them (D-068).
|
|
206
|
+
.strict(),
|
|
203
207
|
),
|
|
204
208
|
responses: ok(`A page of ${def.label}, classified values in the body`),
|
|
205
209
|
}),
|
package/src/types.ts
CHANGED
|
@@ -231,6 +231,8 @@ export interface AccessStore {
|
|
|
231
231
|
/** Staff activity or a customer renewal; the renewed expiry, if any. */
|
|
232
232
|
maintain(session: AccessSession, now: number): Promise<number | null>;
|
|
233
233
|
apiKey(presented: string, now: number): Promise<ResolvedAccess | null>;
|
|
234
|
+
/** A CLI session's principal (p0.10 §6.2), or null when the token names no unexpired one. */
|
|
235
|
+
cliSession(token: string, now: number): Promise<ResolvedAccess | null>;
|
|
234
236
|
keyTouchDue(key: { lastUsedAt: number | null }, now: number): boolean;
|
|
235
237
|
touchKey(keyId: string, now: number): Promise<void>;
|
|
236
238
|
/** Counts a failed key verification against the address; false once past the limit (§4.3). */
|
|
@@ -365,6 +367,20 @@ export interface IssuedInvite {
|
|
|
365
367
|
token: string;
|
|
366
368
|
expiresAt: number;
|
|
367
369
|
}
|
|
370
|
+
/** What a credential reference made, kept for ever (p0.10 §6.2). */
|
|
371
|
+
export interface CredentialRefState {
|
|
372
|
+
ref: string;
|
|
373
|
+
kind: "secret" | "publishable" | "storefront";
|
|
374
|
+
state: "used" | "fenced" | "revoked";
|
|
375
|
+
storefrontId: string | null;
|
|
376
|
+
keyId: string | null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export type CredentialRefCreate =
|
|
380
|
+
| { kind: "secret"; name: string; grants: readonly GrantInput[]; locationIds?: readonly string[] }
|
|
381
|
+
| { kind: "publishable"; name: string; storefrontId: string }
|
|
382
|
+
| { kind: "storefront"; name: string; allowedOrigins: readonly string[] };
|
|
383
|
+
|
|
368
384
|
export interface IssuedKey {
|
|
369
385
|
id: string;
|
|
370
386
|
key: string;
|
|
@@ -443,6 +459,8 @@ export interface AdminStore {
|
|
|
443
459
|
storefrontId: string,
|
|
444
460
|
allowedOrigins: readonly string[],
|
|
445
461
|
req?: AdminRequest,
|
|
462
|
+
/** The list the caller read: the write is refused CONFLICT if it changed since. */
|
|
463
|
+
expected?: readonly string[],
|
|
446
464
|
): Promise<void>;
|
|
447
465
|
createSecretKey(
|
|
448
466
|
ctx: AccessContext,
|
|
@@ -461,6 +479,20 @@ export interface AdminStore {
|
|
|
461
479
|
req?: AdminRequest,
|
|
462
480
|
storefrontId?: string,
|
|
463
481
|
): Promise<{ id: string; revokedAt: number | null }>;
|
|
482
|
+
/** A key made under a caller-chosen ref: at most one, ever (p0.10 §6.2). */
|
|
483
|
+
createByRef(
|
|
484
|
+
ctx: AccessContext,
|
|
485
|
+
ref: string,
|
|
486
|
+
input: CredentialRefCreate,
|
|
487
|
+
req?: AdminRequest,
|
|
488
|
+
): Promise<CredentialRefState & IssuedKey>;
|
|
489
|
+
/** Settles a ref for good: fenced when unused, its key revoked when used. */
|
|
490
|
+
voidRef(
|
|
491
|
+
ctx: AccessContext,
|
|
492
|
+
ref: string,
|
|
493
|
+
kind: CredentialRefState["kind"],
|
|
494
|
+
req?: AdminRequest,
|
|
495
|
+
): Promise<CredentialRefState>;
|
|
464
496
|
rotateKey(
|
|
465
497
|
ctx: AccessContext,
|
|
466
498
|
keyId: string,
|