@mnemom/mnemom 0.17.39-next.1 → 0.17.39-next.2
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/commands/auth.d.ts +46 -1
- package/dist/commands/auth.js +51 -2
- package/dist/durable/turn-chain.js +3 -1
- package/dist/host/inbox-delivery.js +3 -1
- package/dist/index.js +3 -2
- package/dist/lib/api.d.ts +19 -0
- package/dist/lib/api.js +8 -0
- package/dist/vendor/durable-contract/index.js +21 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +20 -0
- package/dist/vendor/shared/durable-contract/doors.js +32 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +2 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +2 -0
- package/package.json +1 -1
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom whoami --json`: who is logged in, and which org is active, as ONE
|
|
3
|
+
* JSON object for a program to read. It carries identity only, never a
|
|
4
|
+
* credential: no access or refresh token, no API key, no scope string.
|
|
5
|
+
*
|
|
6
|
+
* {"logged_in": bool, "user_id": str|null, "email": str|null,
|
|
7
|
+
* "key_id": str|null, "org_id": str|null,
|
|
8
|
+
* "active_org": {"org_id", "slug", "name"}|null, "expires_at": ISO|null,
|
|
9
|
+
* "api_url": the API origin this identity was resolved against}
|
|
10
|
+
*
|
|
11
|
+
* A person (an OAuth login) is user_id + email, read from GET /v1/auth/session.
|
|
12
|
+
* An API key is key_id + org_id (its own org), read from GET
|
|
13
|
+
* /v1/api-keys/whoami; its user_id is always null, because a key's owning
|
|
14
|
+
* person is not the caller and a reader must key a key on key_id, never on
|
|
15
|
+
* that person. When the identity read fails, user_id, email, key_id and org_id
|
|
16
|
+
* are null: never a stale stored value.
|
|
17
|
+
*
|
|
18
|
+
* api_url is getApiUrl() in this process (--api / MNEMOM_API_URL > MNEMOM_RING
|
|
19
|
+
* > MNEMOM_ENV). Identities never cross environments (test, us-1 and
|
|
20
|
+
* production each have their own), so a reader pins one origin and refuses a
|
|
21
|
+
* whoami whose api_url is not it.
|
|
22
|
+
*
|
|
23
|
+
* A reader beside the CLI must never read ~/.mnemom/auth.json or call the API
|
|
24
|
+
* with the stored token itself: the CLI refreshes and rotates its tokens under
|
|
25
|
+
* a lock (MNE-9232), and a second token handler would race that refresh. This
|
|
26
|
+
* is the read such a reader uses instead (develop's install and every acting
|
|
27
|
+
* develop verb, which take their operator and tenant from the login).
|
|
28
|
+
*/
|
|
29
|
+
export interface WhoamiJson {
|
|
30
|
+
logged_in: boolean;
|
|
31
|
+
user_id: string | null;
|
|
32
|
+
email: string | null;
|
|
33
|
+
key_id: string | null;
|
|
34
|
+
org_id: string | null;
|
|
35
|
+
active_org: {
|
|
36
|
+
org_id: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
name: string | null;
|
|
39
|
+
} | null;
|
|
40
|
+
expires_at: string | null;
|
|
41
|
+
api_url: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function whoamiJson(): Promise<WhoamiJson>;
|
|
1
44
|
export declare function loginCommand(options?: {
|
|
2
45
|
noBrowser?: boolean;
|
|
3
46
|
}): Promise<void>;
|
|
4
47
|
export declare function logoutCommand(): Promise<void>;
|
|
5
|
-
export declare function whoamiCommand(
|
|
48
|
+
export declare function whoamiCommand(options?: {
|
|
49
|
+
json?: boolean;
|
|
50
|
+
}): Promise<void>;
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
import { getAuthInfo, clearAuthTokens, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
|
|
2
|
-
import { getMe } from "../lib/api.js";
|
|
2
|
+
import { getApiKeyWhoami, getMe, getSession } from "../lib/api.js";
|
|
3
|
+
import { getActiveOrg } from "../lib/cli-config.js";
|
|
4
|
+
import { getApiUrl } from "../lib/config.js";
|
|
3
5
|
import { fmt } from "../lib/format.js";
|
|
6
|
+
export async function whoamiJson() {
|
|
7
|
+
const active = getActiveOrg();
|
|
8
|
+
const active_org = active
|
|
9
|
+
? { org_id: active.org_id, slug: active.slug, name: active.name ?? null }
|
|
10
|
+
: null;
|
|
11
|
+
const out = {
|
|
12
|
+
logged_in: false,
|
|
13
|
+
user_id: null,
|
|
14
|
+
email: null,
|
|
15
|
+
key_id: null,
|
|
16
|
+
org_id: null,
|
|
17
|
+
active_org,
|
|
18
|
+
expires_at: null,
|
|
19
|
+
api_url: getApiUrl(),
|
|
20
|
+
};
|
|
21
|
+
const cred = await resolveAuth();
|
|
22
|
+
if (cred.type === "none")
|
|
23
|
+
return out;
|
|
24
|
+
if (cred.type === "api-key") {
|
|
25
|
+
// A key is keyed on its own id and org, never on its owner's user id.
|
|
26
|
+
const key = await getApiKeyWhoami().catch(() => null);
|
|
27
|
+
return { ...out, logged_in: true, key_id: key?.key_id ?? null, org_id: key?.org_id ?? null };
|
|
28
|
+
}
|
|
29
|
+
if (!getAuthInfo())
|
|
30
|
+
return out;
|
|
31
|
+
// The OAuth token carries no identity: ask the API (GET /v1/auth/session, a
|
|
32
|
+
// pure read, unlike /v1/auth/me's billing and SSO JIT writes), which goes
|
|
33
|
+
// through the CLI's own refresh. A failed read leaves the identity null: a
|
|
34
|
+
// stored legacy value could belong to another session or environment.
|
|
35
|
+
const session = await getSession().catch(() => null);
|
|
36
|
+
const me = session ? { user_id: session.user.id, email: session.user.email ?? null } : null;
|
|
37
|
+
// Read the grant after that call: a refresh inside it moves expiresAt.
|
|
38
|
+
const auth = getAuthInfo();
|
|
39
|
+
if (!auth)
|
|
40
|
+
return out;
|
|
41
|
+
return {
|
|
42
|
+
...out,
|
|
43
|
+
logged_in: true,
|
|
44
|
+
user_id: me?.user_id ?? null,
|
|
45
|
+
email: me?.email ?? null,
|
|
46
|
+
expires_at: new Date(auth.expiresAt * 1000).toISOString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
4
49
|
export async function loginCommand(options = {}) {
|
|
5
50
|
try {
|
|
6
51
|
// --no-browser → RFC 8628 device flow (headless / SSH / no local browser).
|
|
@@ -53,7 +98,11 @@ export async function logoutCommand() {
|
|
|
53
98
|
clearAuthTokens();
|
|
54
99
|
console.log(fmt.success("Logged out."));
|
|
55
100
|
}
|
|
56
|
-
export async function whoamiCommand() {
|
|
101
|
+
export async function whoamiCommand(options = {}) {
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify(await whoamiJson()));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
57
106
|
const cred = await resolveAuth();
|
|
58
107
|
if (cred.type === "none") {
|
|
59
108
|
console.log("\nNot logged in. Run `mnemom login` to authenticate.\n");
|
|
@@ -86,7 +86,9 @@ export function entryProducer(e) {
|
|
|
86
86
|
return typeof p === "string" && TURN_PRODUCER_RE.test(p) ? p : undefined;
|
|
87
87
|
}
|
|
88
88
|
/** Claude Code's open tag for a bracketed paste, which may lead a pasted prompt's text. */
|
|
89
|
-
|
|
89
|
+
// Claude Code may record the paste after blank lines ("\n\n<pasted_content id=...>"); a delivery the marker
|
|
90
|
+
// misses there is never confirmed and gets pasted again (seen live: 4 pastes of one message).
|
|
91
|
+
const PASTE_OPEN_RE = /^\n*<pasted_content id="[^"\n]{0,128}">\n?/;
|
|
90
92
|
/**
|
|
91
93
|
* The inbox sha an entry answers (S1 amendment 60): the `[mnemom inbox <sha>]` marker on the first
|
|
92
94
|
* line of the entry's first text (after Claude Code's paste open tag, as the host daemon delivers by
|
|
@@ -72,7 +72,9 @@ export const HANDLED_MAX = 32;
|
|
|
72
72
|
/** claude's input line, and the footer it shows while a turn runs. */
|
|
73
73
|
const PROMPT_MARK = "❯";
|
|
74
74
|
const RUNNING_RE = /esc to interrupt/i;
|
|
75
|
-
|
|
75
|
+
// Claude Code may record the paste after blank lines ("\n\n<pasted_content id=...>"); a delivery the marker
|
|
76
|
+
// misses there is never confirmed and gets pasted again (seen live: 4 pastes of one message).
|
|
77
|
+
const PASTE_OPEN_RE = /^\n*<pasted_content id="[^"\n]{0,128}">\n?/;
|
|
76
78
|
function firstText(r) {
|
|
77
79
|
const c = r.message?.content;
|
|
78
80
|
if (typeof c === "string")
|
package/dist/index.js
CHANGED
|
@@ -1976,9 +1976,10 @@ program
|
|
|
1976
1976
|
program
|
|
1977
1977
|
.command("whoami")
|
|
1978
1978
|
.description("Show current authentication status")
|
|
1979
|
-
.
|
|
1979
|
+
.option("--json", "Print who is logged in as one JSON object: {logged_in, user_id, email, active_org, expires_at} (never a token)")
|
|
1980
|
+
.action(async (options) => {
|
|
1980
1981
|
try {
|
|
1981
|
-
await whoamiCommand();
|
|
1982
|
+
await whoamiCommand({ json: options.json });
|
|
1982
1983
|
}
|
|
1983
1984
|
catch (error) {
|
|
1984
1985
|
console.error("Error:", error instanceof Error ? error.message : error);
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -426,6 +426,25 @@ export interface MeResponse {
|
|
|
426
426
|
} | null;
|
|
427
427
|
}
|
|
428
428
|
export declare function getMe(): Promise<MeResponse>;
|
|
429
|
+
/** Who the credential belongs to — `GET /v1/auth/session`. A pure read for a
|
|
430
|
+
* bearer (no billing-account ensure and no SSO JIT provisioning, which
|
|
431
|
+
* `GET /v1/auth/me` runs on every call), so an identity check changes nothing. */
|
|
432
|
+
export interface SessionResponse {
|
|
433
|
+
user: {
|
|
434
|
+
id: string;
|
|
435
|
+
email?: string | null;
|
|
436
|
+
};
|
|
437
|
+
method?: string;
|
|
438
|
+
}
|
|
439
|
+
export declare function getSession(): Promise<SessionResponse>;
|
|
440
|
+
/** What an API key is — `GET /v1/api-keys/whoami` (ADR-049). A read; the key's
|
|
441
|
+
* own id and org, which is how a reader keys a key caller. */
|
|
442
|
+
export interface ApiKeyWhoamiResponse {
|
|
443
|
+
key_id: string | null;
|
|
444
|
+
org_id: string | null;
|
|
445
|
+
auth_method?: string;
|
|
446
|
+
}
|
|
447
|
+
export declare function getApiKeyWhoami(): Promise<ApiKeyWhoamiResponse>;
|
|
429
448
|
/**
|
|
430
449
|
* The org's Mnemom Units (MU) balance — `GET /v1/billing/mu` (the frozen mu
|
|
431
450
|
* v0.1 balance, values as decimal strings). Amounts come in both whole MU
|
package/dist/lib/api.js
CHANGED
|
@@ -440,6 +440,14 @@ export async function getMyPersonalOrg() {
|
|
|
440
440
|
export async function getMe() {
|
|
441
441
|
return gFetch("/v1/auth/me", {}, { fallback: "Could not read your identity" });
|
|
442
442
|
}
|
|
443
|
+
export async function getSession() {
|
|
444
|
+
return gFetch("/v1/auth/session", {}, { fallback: "Could not read your identity" });
|
|
445
|
+
}
|
|
446
|
+
export async function getApiKeyWhoami() {
|
|
447
|
+
return gFetch("/v1/api-keys/whoami", {}, {
|
|
448
|
+
fallback: "Could not read your API key's identity",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
443
451
|
export async function getRecordTotals(orgId, lineageKey, signal) {
|
|
444
452
|
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/record/totals?lineage_key=${encodeURIComponent(lineageKey)}`, signal ? { signal } : {}, { notFoundLabel: "Record totals", fallback: "Could not read record totals" });
|
|
445
453
|
}
|
|
@@ -2406,6 +2406,8 @@ var OIDC_EXCHANGE_BODY_KEYS = ["oidc_token", "adopt_ticket"];
|
|
|
2406
2406
|
var OIDC_TOKEN_MAX = 8192;
|
|
2407
2407
|
var OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
2408
2408
|
var OIDC_WORKFLOW_FILE_RE = /^[A-Za-z0-9_.-]{1,100}\.ya?ml$/;
|
|
2409
|
+
var OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
2410
|
+
var OIDC_WORKFLOW_SET_MAX = 8;
|
|
2409
2411
|
function adoptTicketOk(t) {
|
|
2410
2412
|
return typeof t === "string" && ADOPT_TICKET_RE.test(t);
|
|
2411
2413
|
}
|
|
@@ -2422,9 +2424,22 @@ function oidcPinnedWorkflow(envValue) {
|
|
|
2422
2424
|
const wf = envValue?.trim() ?? "";
|
|
2423
2425
|
return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
|
|
2424
2426
|
}
|
|
2427
|
+
function oidcWorkflowSet(setValue, pinValue) {
|
|
2428
|
+
if (setValue === void 0) {
|
|
2429
|
+
const pin = oidcPinnedWorkflow(pinValue);
|
|
2430
|
+
return pin === null ? null : [pin];
|
|
2431
|
+
}
|
|
2432
|
+
const names = setValue.split(",").map((n) => n.trim());
|
|
2433
|
+
if (names.some((n) => !OIDC_WORKFLOW_FILE_RE.test(n) || n.includes(".."))) return null;
|
|
2434
|
+
const set = [...new Set(names)];
|
|
2435
|
+
return set.length > OIDC_WORKFLOW_SET_MAX ? null : set;
|
|
2436
|
+
}
|
|
2425
2437
|
function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defaultBranch) {
|
|
2426
2438
|
return typeof jobWorkflowRef === "string" && jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`;
|
|
2427
2439
|
}
|
|
2440
|
+
function jobWorkflowRefInSet(jobWorkflowRef, repository, workflowFiles, defaultBranch) {
|
|
2441
|
+
return workflowFiles.some((f2) => jobWorkflowRefOk(jobWorkflowRef, repository, f2, defaultBranch));
|
|
2442
|
+
}
|
|
2428
2443
|
var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
2429
2444
|
var LIVE_LEASE_CAP_MIN = 1;
|
|
2430
2445
|
var LIVE_LEASE_CAP_MAX = 1e3;
|
|
@@ -5908,6 +5923,8 @@ var FREEZE_RECORD = Object.freeze({
|
|
|
5908
5923
|
OIDC_TOKEN_MAX,
|
|
5909
5924
|
OIDC_WORKFLOW_ENV,
|
|
5910
5925
|
OIDC_WORKFLOW_FILE_PATTERN: OIDC_WORKFLOW_FILE_RE.source,
|
|
5926
|
+
OIDC_WORKFLOW_SET_ENV,
|
|
5927
|
+
OIDC_WORKFLOW_SET_MAX,
|
|
5911
5928
|
READY_SESSIONS_LIMIT_MIN,
|
|
5912
5929
|
READY_SESSIONS_LIMIT_MAX,
|
|
5913
5930
|
// amendment 64
|
|
@@ -6616,6 +6633,8 @@ export {
|
|
|
6616
6633
|
OIDC_TOKEN_MAX,
|
|
6617
6634
|
OIDC_WORKFLOW_ENV,
|
|
6618
6635
|
OIDC_WORKFLOW_FILE_RE,
|
|
6636
|
+
OIDC_WORKFLOW_SET_ENV,
|
|
6637
|
+
OIDC_WORKFLOW_SET_MAX,
|
|
6619
6638
|
OLD_ROUTE_BRANCH,
|
|
6620
6639
|
OLD_ROUTE_REFRESH,
|
|
6621
6640
|
ORG_CAP_LOCK_PREFIX,
|
|
@@ -6928,6 +6947,7 @@ export {
|
|
|
6928
6947
|
isValidSessionName,
|
|
6929
6948
|
isValidStateReason,
|
|
6930
6949
|
isWellFormedLeaseKey,
|
|
6950
|
+
jobWorkflowRefInSet,
|
|
6931
6951
|
jobWorkflowRefOk,
|
|
6932
6952
|
lagBucket,
|
|
6933
6953
|
lintDurableOnly,
|
|
@@ -6943,6 +6963,7 @@ export {
|
|
|
6943
6963
|
oidcExchangeBodyOk,
|
|
6944
6964
|
oidcPinnedWorkflow,
|
|
6945
6965
|
oidcRunClaimsOk,
|
|
6966
|
+
oidcWorkflowSet,
|
|
6946
6967
|
openContract,
|
|
6947
6968
|
ownerSessionViewOk,
|
|
6948
6969
|
ownerVerbDecision,
|
|
@@ -83,6 +83,16 @@ export declare const OIDC_TOKEN_MAX = 8192;
|
|
|
83
83
|
export declare const OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
84
84
|
/** A workflow file name under .github/workflows: no path, no query. */
|
|
85
85
|
export declare const OIDC_WORKFLOW_FILE_RE: RegExp;
|
|
86
|
+
/**
|
|
87
|
+
* Amendment 66, part 2 (Durable lead ruling (a), 2026-11-09): the api env var naming the set of
|
|
88
|
+
* workflow files the exchange accepts, comma-separated bare file names. Unset, the set is
|
|
89
|
+
* OIDC_WORKFLOW_ENV's one file, exactly as before. Set, it replaces that pin for the exchange only
|
|
90
|
+
* (the wake-up still dispatches OIDC_WORKFLOW_ENV's file), and a set that is empty or holds any
|
|
91
|
+
* entry that is not a workflow file name (a wildcard, a "/", a "..") turns the exchange dark.
|
|
92
|
+
*/
|
|
93
|
+
export declare const OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
94
|
+
/** The most workflow files the set may name. */
|
|
95
|
+
export declare const OIDC_WORKFLOW_SET_MAX = 8;
|
|
86
96
|
/** True when `t` has an adopt ticket's shape. */
|
|
87
97
|
export declare function adoptTicketOk(t: unknown): t is string;
|
|
88
98
|
/**
|
|
@@ -93,6 +103,14 @@ export declare function adoptTicketOk(t: unknown): t is string;
|
|
|
93
103
|
export declare function oidcExchangeBodyOk(v: unknown): boolean;
|
|
94
104
|
/** The pinned workflow file from the env value, or null (then the exchange is dark). */
|
|
95
105
|
export declare function oidcPinnedWorkflow(envValue: string | undefined): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* The workflow files the exchange accepts, or null (then the exchange is dark).
|
|
108
|
+
* `setValue` is OIDC_WORKFLOW_SET_ENV's value, `pinValue` OIDC_WORKFLOW_ENV's. With the set unset
|
|
109
|
+
* the answer is the one pinned file, as oidcPinnedWorkflow reads it. With it set, every
|
|
110
|
+
* comma-separated entry, trimmed, must be a workflow file name with no "..", and there must be one
|
|
111
|
+
* to OIDC_WORKFLOW_SET_MAX of them; anything else is null, never a partial set. Duplicates collapse.
|
|
112
|
+
*/
|
|
113
|
+
export declare function oidcWorkflowSet(setValue: string | undefined, pinValue: string | undefined): readonly string[] | null;
|
|
96
114
|
/**
|
|
97
115
|
* The workflow pin: the token's job_workflow_ref must be exactly
|
|
98
116
|
* `<repository>/.github/workflows/<pinned file>@refs/heads/<default branch>`, with `repository`
|
|
@@ -100,6 +118,8 @@ export declare function oidcPinnedWorkflow(envValue: string | undefined): string
|
|
|
100
118
|
* branch, a tag or sha ref) is refused ref_not_allowed.
|
|
101
119
|
*/
|
|
102
120
|
export declare function jobWorkflowRefOk(jobWorkflowRef: unknown, repository: string, workflowFile: string, defaultBranch: string): boolean;
|
|
121
|
+
/** jobWorkflowRefOk for any one file of an oidcWorkflowSet answer (amendment 66, part 2). */
|
|
122
|
+
export declare function jobWorkflowRefInSet(jobWorkflowRef: unknown, repository: string, workflowFiles: readonly string[], defaultBranch: string): boolean;
|
|
103
123
|
/** durable_org_policy.limits keys (34.05 (1)); live_lease_cap is optional. */
|
|
104
124
|
export declare const ORG_POLICY_LIMIT_KEYS: readonly ["per_session_budget_usd", "live_lease_cap"];
|
|
105
125
|
/** live_lease_cap and the registry default share this range. */
|
|
@@ -113,6 +113,16 @@ export const OIDC_TOKEN_MAX = 8192;
|
|
|
113
113
|
export const OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
114
114
|
/** A workflow file name under .github/workflows: no path, no query. */
|
|
115
115
|
export const OIDC_WORKFLOW_FILE_RE = /^[A-Za-z0-9_.-]{1,100}\.ya?ml$/;
|
|
116
|
+
/**
|
|
117
|
+
* Amendment 66, part 2 (Durable lead ruling (a), 2026-11-09): the api env var naming the set of
|
|
118
|
+
* workflow files the exchange accepts, comma-separated bare file names. Unset, the set is
|
|
119
|
+
* OIDC_WORKFLOW_ENV's one file, exactly as before. Set, it replaces that pin for the exchange only
|
|
120
|
+
* (the wake-up still dispatches OIDC_WORKFLOW_ENV's file), and a set that is empty or holds any
|
|
121
|
+
* entry that is not a workflow file name (a wildcard, a "/", a "..") turns the exchange dark.
|
|
122
|
+
*/
|
|
123
|
+
export const OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
124
|
+
/** The most workflow files the set may name. */
|
|
125
|
+
export const OIDC_WORKFLOW_SET_MAX = 8;
|
|
116
126
|
/** True when `t` has an adopt ticket's shape. */
|
|
117
127
|
export function adoptTicketOk(t) {
|
|
118
128
|
return typeof t === "string" && ADOPT_TICKET_RE.test(t);
|
|
@@ -138,6 +148,24 @@ export function oidcPinnedWorkflow(envValue) {
|
|
|
138
148
|
const wf = envValue?.trim() ?? "";
|
|
139
149
|
return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
|
|
140
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The workflow files the exchange accepts, or null (then the exchange is dark).
|
|
153
|
+
* `setValue` is OIDC_WORKFLOW_SET_ENV's value, `pinValue` OIDC_WORKFLOW_ENV's. With the set unset
|
|
154
|
+
* the answer is the one pinned file, as oidcPinnedWorkflow reads it. With it set, every
|
|
155
|
+
* comma-separated entry, trimmed, must be a workflow file name with no "..", and there must be one
|
|
156
|
+
* to OIDC_WORKFLOW_SET_MAX of them; anything else is null, never a partial set. Duplicates collapse.
|
|
157
|
+
*/
|
|
158
|
+
export function oidcWorkflowSet(setValue, pinValue) {
|
|
159
|
+
if (setValue === undefined) {
|
|
160
|
+
const pin = oidcPinnedWorkflow(pinValue);
|
|
161
|
+
return pin === null ? null : [pin];
|
|
162
|
+
}
|
|
163
|
+
const names = setValue.split(",").map((n) => n.trim());
|
|
164
|
+
if (names.some((n) => !OIDC_WORKFLOW_FILE_RE.test(n) || n.includes("..")))
|
|
165
|
+
return null;
|
|
166
|
+
const set = [...new Set(names)];
|
|
167
|
+
return set.length > OIDC_WORKFLOW_SET_MAX ? null : set;
|
|
168
|
+
}
|
|
141
169
|
/**
|
|
142
170
|
* The workflow pin: the token's job_workflow_ref must be exactly
|
|
143
171
|
* `<repository>/.github/workflows/<pinned file>@refs/heads/<default branch>`, with `repository`
|
|
@@ -148,6 +176,10 @@ export function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defau
|
|
|
148
176
|
return (typeof jobWorkflowRef === "string" &&
|
|
149
177
|
jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`);
|
|
150
178
|
}
|
|
179
|
+
/** jobWorkflowRefOk for any one file of an oidcWorkflowSet answer (amendment 66, part 2). */
|
|
180
|
+
export function jobWorkflowRefInSet(jobWorkflowRef, repository, workflowFiles, defaultBranch) {
|
|
181
|
+
return workflowFiles.some((f) => jobWorkflowRefOk(jobWorkflowRef, repository, f, defaultBranch));
|
|
182
|
+
}
|
|
151
183
|
// ---- Per-org live-lease cap (rulings 34.05, 34.05.1) ----------------------------------------
|
|
152
184
|
/** durable_org_policy.limits keys (34.05 (1)); live_lease_cap is optional. */
|
|
153
185
|
export const ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
@@ -128,6 +128,8 @@ export declare const FREEZE_RECORD: Readonly<{
|
|
|
128
128
|
OIDC_TOKEN_MAX: 8192;
|
|
129
129
|
OIDC_WORKFLOW_ENV: "DISPATCH_WAKEUP_WORKFLOW";
|
|
130
130
|
OIDC_WORKFLOW_FILE_PATTERN: string;
|
|
131
|
+
OIDC_WORKFLOW_SET_ENV: "DURABLE_OIDC_WORKFLOWS";
|
|
132
|
+
OIDC_WORKFLOW_SET_MAX: 8;
|
|
131
133
|
READY_SESSIONS_LIMIT_MIN: 1;
|
|
132
134
|
READY_SESSIONS_LIMIT_MAX: 50;
|
|
133
135
|
ADOPT_SOURCE_REF_MAX: 500;
|
|
@@ -159,6 +159,8 @@ export const FREEZE_RECORD = Object.freeze({
|
|
|
159
159
|
OIDC_TOKEN_MAX: D.OIDC_TOKEN_MAX,
|
|
160
160
|
OIDC_WORKFLOW_ENV: D.OIDC_WORKFLOW_ENV,
|
|
161
161
|
OIDC_WORKFLOW_FILE_PATTERN: D.OIDC_WORKFLOW_FILE_RE.source,
|
|
162
|
+
OIDC_WORKFLOW_SET_ENV: D.OIDC_WORKFLOW_SET_ENV,
|
|
163
|
+
OIDC_WORKFLOW_SET_MAX: D.OIDC_WORKFLOW_SET_MAX,
|
|
162
164
|
READY_SESSIONS_LIMIT_MIN: D.READY_SESSIONS_LIMIT_MIN,
|
|
163
165
|
READY_SESSIONS_LIMIT_MAX: D.READY_SESSIONS_LIMIT_MAX,
|
|
164
166
|
// amendment 64
|