@payez/next-mvp 4.8.3 → 4.8.5

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 CHANGED
@@ -499,6 +499,33 @@ export function Providers({ children }: { children: React.ReactNode }) {
499
499
 
500
500
  ---
501
501
 
502
+ ## Register an agent (REG-002)
503
+
504
+ `@payez/next-mvp/agents` is a typed client over the tenant-admin agent door (`POST/GET api/client/agents`). The caller must be a tenant admin with completed MFA; the client (tenant) comes from the token, so there is no client id to pass.
505
+
506
+ ```ts
507
+ import { createAgentRegistryClient, AgentRegistryError } from '@payez/next-mvp/agents';
508
+
509
+ const agents = createAgentRegistryClient({
510
+ baseUrl: process.env.IDP_URL!, // absolute origin of the identity service
511
+ accessToken: () => getAdminAccessToken(), // a string, or a function returning one
512
+ });
513
+
514
+ try {
515
+ const { item } = await agents.register({ user_name: 'build-bot', owner_user_id: 42, host_class: 'rig_pane' });
516
+ // item.status.code === 'PENDING': the first mint is refused AGENT_PENDING until an admin activates it.
517
+ } catch (e) {
518
+ if (e instanceof AgentRegistryError) {
519
+ // e.code is Core's code as-is: VALIDATION_ERROR, OWNER_NOT_ELIGIBLE, OWNER_NOT_MEMBER, AGENT_NAME_TAKEN, ...
520
+ // e.status is the HTTP status (0 = no response). Nothing is retried; a 409 AGENT_NAME_TAKEN stays a 409.
521
+ }
522
+ throw e;
523
+ }
524
+
525
+ const { items } = await agents.list(); // every agent of your client
526
+ const { item: one } = await agents.get(41);
527
+ ```
528
+
502
529
  ## API Routes
503
530
 
504
531
  Access pre-built API handlers:
@@ -0,0 +1 @@
1
+ export * from './registry';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./registry"), exports);
@@ -0,0 +1,73 @@
1
+ /** Core PayEzEndpoints.AgentTokens.AllowedHostClasses (PayEzEndpoints.cs:187,194,228). The server is the authority; this is for typing only. */
2
+ export declare const AGENT_HOST_CLASSES: readonly ["rig_pane", "contractor"];
3
+ export type AgentHostClass = (typeof AGENT_HOST_CLASSES)[number];
4
+ export interface RegisterAgentInput {
5
+ /** 3-64 chars, letters/digits/'.'/'_'/'-', starting with a letter or digit; unique across the IdP. */
6
+ user_name: string;
7
+ /** A person (user id) who is an approved, active member of this client. */
8
+ owner_user_id: number;
9
+ host_class: AgentHostClass;
10
+ }
11
+ export interface AgentRecord {
12
+ id: number;
13
+ user_name: string;
14
+ owner: {
15
+ kind: 'person';
16
+ user_id: number;
17
+ display_name: string | null;
18
+ approved: boolean;
19
+ } | null;
20
+ status: {
21
+ code: string;
22
+ reason: string | null;
23
+ };
24
+ host_class: string | null;
25
+ agent_profile_id: number | null;
26
+ }
27
+ export interface MintableApp {
28
+ id: number;
29
+ name: string;
30
+ allowed_scopes: unknown;
31
+ }
32
+ export interface RegisterAgentResult {
33
+ item: AgentRecord | null;
34
+ created: boolean;
35
+ audited: boolean;
36
+ }
37
+ export interface ListAgentsResult {
38
+ client_id: number;
39
+ demo_token_ready: boolean | null;
40
+ items: AgentRecord[];
41
+ apps: MintableApp[];
42
+ }
43
+ export interface GetAgentResult {
44
+ item: AgentRecord;
45
+ apps: MintableApp[];
46
+ }
47
+ /** Codes this door is known to answer. `string` stays in the union: an unknown code is surfaced, not hidden. */
48
+ export type AgentRegistryErrorCode = 'VALIDATION_ERROR' | 'OWNER_NOT_ELIGIBLE' | 'OWNER_NOT_MEMBER' | 'AGENT_NAME_TAKEN' | 'AUDIT_WRITE_FAILED_NOT_APPLIED' | 'MISSING_CLIENT_ID' | 'UNAUTHORIZED' | 'IDP_UNREACHABLE' | 'NETWORK_ERROR' | 'BAD_RESPONSE' | (string & {});
49
+ export declare class AgentRegistryError extends Error {
50
+ readonly code: AgentRegistryErrorCode;
51
+ /** HTTP status; 0 when no response arrived. */
52
+ readonly status: number;
53
+ readonly requestId?: string;
54
+ readonly details?: unknown;
55
+ constructor(code: AgentRegistryErrorCode, message: string, status: number, extra?: {
56
+ requestId?: string;
57
+ details?: unknown;
58
+ });
59
+ }
60
+ export interface AgentRegistryClientOptions {
61
+ /** Absolute origin of the identity service (e.g. IDP_URL). */
62
+ baseUrl: string;
63
+ /** A tenant-admin access token (completed MFA), or a function that returns one. */
64
+ accessToken: string | (() => string | Promise<string>);
65
+ /** Override for tests; defaults to bearerFetch (global fetch, never follows a redirect). */
66
+ fetch?: (input: string, init: RequestInit) => Promise<Response>;
67
+ }
68
+ export interface AgentRegistryClient {
69
+ register(input: RegisterAgentInput): Promise<RegisterAgentResult>;
70
+ list(): Promise<ListAgentsResult>;
71
+ get(id: number): Promise<GetAgentResult>;
72
+ }
73
+ export declare function createAgentRegistryClient(options: AgentRegistryClientOptions): AgentRegistryClient;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentRegistryError = exports.AGENT_HOST_CLASSES = void 0;
4
+ exports.createAgentRegistryClient = createAgentRegistryClient;
5
+ /**
6
+ * REG-002 (Keel Registry): the SDK half of "Registration API and SDK".
7
+ *
8
+ * A typed client over the tenant-admin agent door in PayEz-Core:
9
+ * PayEz.Apis/PayEz.External.Id.Api/Controllers/ClientAgentsController.cs @ 22d44a34c
10
+ * POST api/client/agents Register (:332-367) body { user_name, owner_user_id, host_class } -> 201 { item, created, audited }
11
+ * GET api/client/agents List (:92-107) -> { client_id, demo_token_ready, items, apps }
12
+ * GET api/client/agents/{id} Get (:126-133) -> { item, apps } | 404
13
+ * Gate: payez_admin / idp_admin_ui_graft_admin with completed MFA (:39-40). The client comes from the TOKEN, never from the
14
+ * body, so this client has no client-id argument and cannot register into another client.
15
+ *
16
+ * Contract this client keeps:
17
+ * - a refusal is thrown as an AgentRegistryError carrying the server's `code` AS-IS (VALIDATION_ERROR, OWNER_NOT_ELIGIBLE,
18
+ * OWNER_NOT_MEMBER, AGENT_NAME_TAKEN, AUDIT_WRITE_FAILED_NOT_APPLIED, MISSING_CLIENT_ID, UNAUTHORIZED, ...). It is never
19
+ * re-worded, mapped, or swallowed.
20
+ * - NO RETRY, of any status. A 409 AGENT_NAME_TAKEN means the name exists; sending it again can only repeat the answer, and a
21
+ * register that timed out may have been applied, so the caller (who can read the list) decides, not this client.
22
+ * - no validation of the body before sending: the server's validation is the authority and its code is what the caller gets.
23
+ * - the bearer never follows a redirect (bearerFetch) and is never put in an error, a message or a log line.
24
+ *
25
+ * A newly registered agent is PENDING until an admin activates it (PAY-2069); its first mint is refused AGENT_PENDING.
26
+ */
27
+ const bearer_fetch_1 = require("../lib/bearer-fetch");
28
+ const idp_url_guard_1 = require("../lib/idp-url-guard");
29
+ /** Core PayEzEndpoints.AgentTokens.AllowedHostClasses (PayEzEndpoints.cs:187,194,228). The server is the authority; this is for typing only. */
30
+ exports.AGENT_HOST_CLASSES = ['rig_pane', 'contractor'];
31
+ class AgentRegistryError extends Error {
32
+ code;
33
+ /** HTTP status; 0 when no response arrived. */
34
+ status;
35
+ requestId;
36
+ details;
37
+ constructor(code, message, status, extra) {
38
+ super(message);
39
+ this.name = 'AgentRegistryError';
40
+ this.code = code;
41
+ this.status = status;
42
+ this.requestId = extra?.requestId;
43
+ this.details = extra?.details;
44
+ }
45
+ }
46
+ exports.AgentRegistryError = AgentRegistryError;
47
+ function createAgentRegistryClient(options) {
48
+ (0, idp_url_guard_1.assertAbsoluteIdpUrl)(options.baseUrl);
49
+ const base = options.baseUrl.replace(/\/+$/, '');
50
+ const send = options.fetch ?? ((input, init) => (0, bearer_fetch_1.bearerFetch)(input, init));
51
+ async function call(path, init) {
52
+ const token = typeof options.accessToken === 'function' ? await options.accessToken() : options.accessToken;
53
+ let res;
54
+ try {
55
+ res = await send(`${base}${path}`, {
56
+ method: init.method,
57
+ headers: {
58
+ Accept: 'application/json',
59
+ ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
60
+ Authorization: `Bearer ${token}`,
61
+ },
62
+ ...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
63
+ cache: 'no-store',
64
+ });
65
+ }
66
+ catch {
67
+ // Fixed text: a transport error's own message can carry the URL or headers.
68
+ // A POST that failed in transit may still have been applied (the server can answer after the client gave up), so for register the text says the
69
+ // outcome is unknown and to read the list; it is never retried here. A GET changes nothing, so it keeps the plain text.
70
+ throw new AgentRegistryError('NETWORK_ERROR', init.method === 'POST'
71
+ ? 'The identity service could not be reached; the outcome is unknown, so read the list before registering again'
72
+ : 'The identity service could not be reached', 0);
73
+ }
74
+ let json = null;
75
+ try {
76
+ json = await res.json();
77
+ }
78
+ catch {
79
+ json = null;
80
+ }
81
+ if (!res.ok) {
82
+ const e = json?.error;
83
+ throw new AgentRegistryError(typeof e?.code === 'string' ? e.code : `HTTP_${res.status}`, typeof e?.message === 'string' ? e.message : `The identity service answered ${res.status}`, res.status, { requestId: json?.request_id ?? e?.support?.request_id, details: e?.details });
84
+ }
85
+ const data = json?.data;
86
+ if (json?.success === false || data == null || typeof data !== 'object') {
87
+ throw new AgentRegistryError('BAD_RESPONSE', 'The identity service answered without the expected data', res.status);
88
+ }
89
+ return data;
90
+ }
91
+ return {
92
+ register: (input) => call('/api/client/agents', {
93
+ method: 'POST',
94
+ // Exactly the three fields the door binds; nothing else is forwarded.
95
+ body: { user_name: input.user_name, owner_user_id: input.owner_user_id, host_class: input.host_class },
96
+ }),
97
+ list: () => call('/api/client/agents', { method: 'GET' }),
98
+ get: (id) => call(`/api/client/agents/${encodeURIComponent(String(id))}`, { method: 'GET' }),
99
+ };
100
+ }
@@ -9,6 +9,7 @@ const TraditionalAuthSection_1 = require("./TraditionalAuthSection");
9
9
  const safe_callback_1 = require("../../auth/safe-callback");
10
10
  const authorize_resume_1 = require("../../lib/authorize-resume");
11
11
  const code_step_copy_1 = require("../../lib/code-step-copy");
12
+ const DevCodeNotice_1 = require("./DevCodeNotice");
12
13
  const credential_sign_in_client_1 = require("../../lib/credential-sign-in-client");
13
14
  const inputStyle = {
14
15
  borderColor: 'var(--border-default, #e5e7eb)',
@@ -39,6 +40,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
39
40
  const [fieldError, setFieldError] = (0, react_1.useState)(null);
40
41
  const [codeRequest, setCodeRequest] = (0, react_1.useState)(code_step_copy_1.NO_CODE_REQUEST);
41
42
  const [offerEmailCode, setOfferEmailCode] = (0, react_1.useState)(false);
43
+ const [devCode, setDevCode] = (0, react_1.useState)(null);
42
44
  const [emailCodeMsg, setEmailCodeMsg] = (0, react_1.useState)(null);
43
45
  async function submit(withCode) {
44
46
  setBusy(true);
@@ -56,6 +58,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
56
58
  }
57
59
  if (r.kind === 'two_factor') {
58
60
  setCodeRequest(r.codeRequest);
61
+ setDevCode(r.devCode);
59
62
  setStep('code');
60
63
  return;
61
64
  }
@@ -90,6 +93,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
90
93
  function onBack() {
91
94
  setStep('credentials');
92
95
  setCodeRequest(code_step_copy_1.NO_CODE_REQUEST);
96
+ setDevCode(null);
93
97
  setCode('');
94
98
  setFieldError(null);
95
99
  setError(null);
@@ -103,6 +107,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
103
107
  const r = await (0, credential_sign_in_client_1.requestEmailCode)(fetchImpl);
104
108
  if (r.kind === 'requested') {
105
109
  setCodeRequest(r.codeRequest);
110
+ setDevCode(r.devCode);
106
111
  setError(null);
107
112
  setOfferEmailCode(false);
108
113
  setEmailCodeMsg('We asked for a code to be emailed to you. Enter it above when it arrives.');
@@ -121,7 +126,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
121
126
  return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "credential-sign-in-code-step", className: "text-left", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm mb-4", style: { color: 'var(--text-primary, #111827)' }, children: (0, code_step_copy_1.codeStepInstruction)(email, codeRequest) }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: onCodeSubmit, noValidate: true, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "credential-code", className: "block text-sm font-medium mb-2", style: { color: 'var(--text-primary, #111827)' }, children: "Authentication code" }), (0, jsx_runtime_1.jsx)("input", { id: "credential-code", name: "code", type: "text", inputMode: "numeric", autoComplete: "one-time-code", required: true, placeholder: "000000", value: code, disabled: busy, onChange: (e) => {
122
127
  setCode(e.target.value);
123
128
  setFieldError(null);
124
- }, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: inputStyle })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy, className: "w-full py-3 px-4 rounded-lg font-semibold focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", style: { background: 'var(--btn-primary-bg, #11B588)', color: 'var(--btn-primary-text, white)' }, children: busy ? 'Verifying…' : 'Verify' })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onBack, disabled: busy, className: "text-sm underline mt-4", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-muted, #6b7280)' }, children: "Back" }), problemLine, (offerEmailCode || emailCodeMsg) && ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "email-code-offer", className: "mt-4", children: [offerEmailCode && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => void askForEmailCode(), disabled: busy, className: "text-sm font-semibold underline", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-primary, #111827)' }, children: "Email me a code instead" })), emailCodeMsg && ((0, jsx_runtime_1.jsx)("p", { role: "status", "data-testid": "email-code-offer-msg", className: "text-sm mt-2", style: { color: 'var(--text-primary, #111827)' }, children: emailCodeMsg }))] }))] }));
129
+ }, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: inputStyle }), (0, jsx_runtime_1.jsx)(DevCodeNotice_1.DevCodeNotice, { code: devCode })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy, className: "w-full py-3 px-4 rounded-lg font-semibold focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", style: { background: 'var(--btn-primary-bg, #11B588)', color: 'var(--btn-primary-text, white)' }, children: busy ? 'Verifying…' : 'Verify' })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onBack, disabled: busy, className: "text-sm underline mt-4", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-muted, #6b7280)' }, children: "Back" }), problemLine, (offerEmailCode || emailCodeMsg) && ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "email-code-offer", className: "mt-4", children: [offerEmailCode && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => void askForEmailCode(), disabled: busy, className: "text-sm font-semibold underline", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-primary, #111827)' }, children: "Email me a code instead" })), emailCodeMsg && ((0, jsx_runtime_1.jsx)("p", { role: "status", "data-testid": "email-code-offer-msg", className: "text-sm mt-2", style: { color: 'var(--text-primary, #111827)' }, children: emailCodeMsg }))] }))] }));
125
130
  }
126
131
  return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "credential-sign-in", className: "text-left", children: [(0, jsx_runtime_1.jsx)(TraditionalAuthSection_1.TraditionalAuthSection, { email: email, password: password, onEmailChange: (v) => {
127
132
  setEmail(v);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * AUTH-045 (BAPert 75627): on a DEV identity service that echoes the emailed sign-in code in its own answer (Email:DevCodeEcho, an
3
+ * IdP setting that is on only in Development), the sign-in form shows that code under the code box, because on a dev rig the email
4
+ * goes to nobody. This component is a PURE PASSTHROUGH of the server's `devVerificationCode`: it renders nothing unless it is given
5
+ * a non-empty string, and CredentialSignInForm gives it nothing but the value the door returned. There is no environment flag in the
6
+ * app, so a production IdP (echo off, field absent) can never make it appear.
7
+ */
8
+ export declare function DevCodeNotice({ code }: {
9
+ code?: string | null;
10
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevCodeNotice = DevCodeNotice;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * AUTH-045 (BAPert 75627): on a DEV identity service that echoes the emailed sign-in code in its own answer (Email:DevCodeEcho, an
7
+ * IdP setting that is on only in Development), the sign-in form shows that code under the code box, because on a dev rig the email
8
+ * goes to nobody. This component is a PURE PASSTHROUGH of the server's `devVerificationCode`: it renders nothing unless it is given
9
+ * a non-empty string, and CredentialSignInForm gives it nothing but the value the door returned. There is no environment flag in the
10
+ * app, so a production IdP (echo off, field absent) can never make it appear.
11
+ */
12
+ function DevCodeNotice({ code }) {
13
+ if (typeof code !== 'string' || code === '')
14
+ return null;
15
+ return ((0, jsx_runtime_1.jsxs)("p", { "data-testid": "dev-sign-in-code", className: "text-sm mt-3", style: { color: 'var(--text-muted, #6b7280)' }, children: ["Development sign-in code: ", (0, jsx_runtime_1.jsx)("strong", { children: code })] }));
16
+ }
@@ -22,6 +22,7 @@ export type CredentialSignInResult = {
22
22
  } | {
23
23
  kind: 'two_factor';
24
24
  codeRequest: CodeRequest;
25
+ devCode: string | null;
25
26
  } | {
26
27
  kind: 'rate_limited';
27
28
  message: string;
@@ -43,6 +43,10 @@ function validateCredentials(email, password) {
43
43
  errors.password = 'Enter your password.';
44
44
  return errors.email || errors.password ? errors : null;
45
45
  }
46
+ /** The dev echo: the door passes the IdP's DevVerificationCode through as `devVerificationCode`. Nothing else is ever a dev code. */
47
+ function readDevCode(body) {
48
+ return typeof body?.devVerificationCode === 'string' && body.devVerificationCode ? body.devVerificationCode : null;
49
+ }
46
50
  function asString(v) {
47
51
  return typeof v === 'string' && v ? v : undefined;
48
52
  }
@@ -58,7 +62,7 @@ async function credentialSignIn(args, fetchImpl = (input, init) => fetch(input,
58
62
  if (res.status === 429)
59
63
  return { kind: 'rate_limited', message: asString(body?.error) ?? exports.MSG_RATE_LIMITED };
60
64
  if (body?.requiresTwoFactor)
61
- return { kind: 'two_factor', codeRequest: (0, code_step_copy_1.readCodeRequest)(body) };
65
+ return { kind: 'two_factor', codeRequest: (0, code_step_copy_1.readCodeRequest)(body), devCode: readDevCode(body) };
62
66
  if (!res.ok || body?.success !== true) {
63
67
  return { kind: 'error', message: asString(body?.error) ?? exports.MSG_CANNOT_SIGN_IN, offerEmailCode: body?.offerEmailCode === true };
64
68
  }
@@ -78,7 +82,7 @@ async function requestEmailCode(fetchImpl = (input, init) => fetch(input, init))
78
82
  return {
79
83
  kind: 'requested',
80
84
  codeRequest: (0, code_step_copy_1.readCodeRequest)(body),
81
- devCode: typeof body?.devVerificationCode === 'string' && body.devVerificationCode ? body.devVerificationCode : null,
85
+ devCode: readDevCode(body),
82
86
  };
83
87
  }
84
88
  return { kind: 'error', message: asString(body?.error) ?? exports.MSG_EMAIL_CODE_FAILED };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.8.3",
3
+ "version": "4.8.5",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -810,6 +810,11 @@
810
810
  "require": "./dist/hooks/useViabilitySession.js",
811
811
  "default": "./dist/hooks/useViabilitySession.js"
812
812
  },
813
+ "./agents": {
814
+ "types": "./dist/agents/index.d.ts",
815
+ "require": "./dist/agents/index.js",
816
+ "default": "./dist/agents/index.js"
817
+ },
813
818
  "./vibe": {
814
819
  "types": "./dist/vibe/index.d.ts",
815
820
  "require": "./dist/vibe/index.js",
@@ -0,0 +1 @@
1
+ export * from './registry';
@@ -0,0 +1,179 @@
1
+ /**
2
+ * REG-002 (Keel Registry): the SDK half of "Registration API and SDK".
3
+ *
4
+ * A typed client over the tenant-admin agent door in PayEz-Core:
5
+ * PayEz.Apis/PayEz.External.Id.Api/Controllers/ClientAgentsController.cs @ 22d44a34c
6
+ * POST api/client/agents Register (:332-367) body { user_name, owner_user_id, host_class } -> 201 { item, created, audited }
7
+ * GET api/client/agents List (:92-107) -> { client_id, demo_token_ready, items, apps }
8
+ * GET api/client/agents/{id} Get (:126-133) -> { item, apps } | 404
9
+ * Gate: payez_admin / idp_admin_ui_graft_admin with completed MFA (:39-40). The client comes from the TOKEN, never from the
10
+ * body, so this client has no client-id argument and cannot register into another client.
11
+ *
12
+ * Contract this client keeps:
13
+ * - a refusal is thrown as an AgentRegistryError carrying the server's `code` AS-IS (VALIDATION_ERROR, OWNER_NOT_ELIGIBLE,
14
+ * OWNER_NOT_MEMBER, AGENT_NAME_TAKEN, AUDIT_WRITE_FAILED_NOT_APPLIED, MISSING_CLIENT_ID, UNAUTHORIZED, ...). It is never
15
+ * re-worded, mapped, or swallowed.
16
+ * - NO RETRY, of any status. A 409 AGENT_NAME_TAKEN means the name exists; sending it again can only repeat the answer, and a
17
+ * register that timed out may have been applied, so the caller (who can read the list) decides, not this client.
18
+ * - no validation of the body before sending: the server's validation is the authority and its code is what the caller gets.
19
+ * - the bearer never follows a redirect (bearerFetch) and is never put in an error, a message or a log line.
20
+ *
21
+ * A newly registered agent is PENDING until an admin activates it (PAY-2069); its first mint is refused AGENT_PENDING.
22
+ */
23
+ import { bearerFetch } from '../lib/bearer-fetch';
24
+ import { assertAbsoluteIdpUrl } from '../lib/idp-url-guard';
25
+
26
+ /** Core PayEzEndpoints.AgentTokens.AllowedHostClasses (PayEzEndpoints.cs:187,194,228). The server is the authority; this is for typing only. */
27
+ export const AGENT_HOST_CLASSES = ['rig_pane', 'contractor'] as const;
28
+ export type AgentHostClass = (typeof AGENT_HOST_CLASSES)[number];
29
+
30
+ export interface RegisterAgentInput {
31
+ /** 3-64 chars, letters/digits/'.'/'_'/'-', starting with a letter or digit; unique across the IdP. */
32
+ user_name: string;
33
+ /** A person (user id) who is an approved, active member of this client. */
34
+ owner_user_id: number;
35
+ host_class: AgentHostClass;
36
+ }
37
+
38
+ export interface AgentRecord {
39
+ id: number;
40
+ user_name: string;
41
+ owner: { kind: 'person'; user_id: number; display_name: string | null; approved: boolean } | null;
42
+ status: { code: string; reason: string | null };
43
+ host_class: string | null;
44
+ agent_profile_id: number | null;
45
+ }
46
+
47
+ export interface MintableApp {
48
+ id: number;
49
+ name: string;
50
+ allowed_scopes: unknown;
51
+ }
52
+
53
+ export interface RegisterAgentResult {
54
+ item: AgentRecord | null;
55
+ created: boolean;
56
+ audited: boolean;
57
+ }
58
+
59
+ export interface ListAgentsResult {
60
+ client_id: number;
61
+ demo_token_ready: boolean | null;
62
+ items: AgentRecord[];
63
+ apps: MintableApp[];
64
+ }
65
+
66
+ export interface GetAgentResult {
67
+ item: AgentRecord;
68
+ apps: MintableApp[];
69
+ }
70
+
71
+ /** Codes this door is known to answer. `string` stays in the union: an unknown code is surfaced, not hidden. */
72
+ export type AgentRegistryErrorCode =
73
+ | 'VALIDATION_ERROR'
74
+ | 'OWNER_NOT_ELIGIBLE'
75
+ | 'OWNER_NOT_MEMBER'
76
+ | 'AGENT_NAME_TAKEN'
77
+ | 'AUDIT_WRITE_FAILED_NOT_APPLIED'
78
+ | 'MISSING_CLIENT_ID'
79
+ | 'UNAUTHORIZED'
80
+ | 'IDP_UNREACHABLE'
81
+ | 'NETWORK_ERROR'
82
+ | 'BAD_RESPONSE'
83
+ | (string & {});
84
+
85
+ export class AgentRegistryError extends Error {
86
+ readonly code: AgentRegistryErrorCode;
87
+ /** HTTP status; 0 when no response arrived. */
88
+ readonly status: number;
89
+ readonly requestId?: string;
90
+ readonly details?: unknown;
91
+
92
+ constructor(code: AgentRegistryErrorCode, message: string, status: number, extra?: { requestId?: string; details?: unknown }) {
93
+ super(message);
94
+ this.name = 'AgentRegistryError';
95
+ this.code = code;
96
+ this.status = status;
97
+ this.requestId = extra?.requestId;
98
+ this.details = extra?.details;
99
+ }
100
+ }
101
+
102
+ export interface AgentRegistryClientOptions {
103
+ /** Absolute origin of the identity service (e.g. IDP_URL). */
104
+ baseUrl: string;
105
+ /** A tenant-admin access token (completed MFA), or a function that returns one. */
106
+ accessToken: string | (() => string | Promise<string>);
107
+ /** Override for tests; defaults to bearerFetch (global fetch, never follows a redirect). */
108
+ fetch?: (input: string, init: RequestInit) => Promise<Response>;
109
+ }
110
+
111
+ export interface AgentRegistryClient {
112
+ register(input: RegisterAgentInput): Promise<RegisterAgentResult>;
113
+ list(): Promise<ListAgentsResult>;
114
+ get(id: number): Promise<GetAgentResult>;
115
+ }
116
+
117
+ export function createAgentRegistryClient(options: AgentRegistryClientOptions): AgentRegistryClient {
118
+ assertAbsoluteIdpUrl(options.baseUrl);
119
+ const base = options.baseUrl.replace(/\/+$/, '');
120
+ const send = options.fetch ?? ((input: string, init: RequestInit) => bearerFetch(input, init));
121
+
122
+ async function call<T>(path: string, init: { method: 'GET' | 'POST'; body?: unknown }): Promise<T> {
123
+ const token = typeof options.accessToken === 'function' ? await options.accessToken() : options.accessToken;
124
+ let res: Response;
125
+ try {
126
+ res = await send(`${base}${path}`, {
127
+ method: init.method,
128
+ headers: {
129
+ Accept: 'application/json',
130
+ ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
131
+ Authorization: `Bearer ${token}`,
132
+ },
133
+ ...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
134
+ cache: 'no-store',
135
+ });
136
+ } catch {
137
+ // Fixed text: a transport error's own message can carry the URL or headers.
138
+ // A POST that failed in transit may still have been applied (the server can answer after the client gave up), so for register the text says the
139
+ // outcome is unknown and to read the list; it is never retried here. A GET changes nothing, so it keeps the plain text.
140
+ throw new AgentRegistryError(
141
+ 'NETWORK_ERROR',
142
+ init.method === 'POST'
143
+ ? 'The identity service could not be reached; the outcome is unknown, so read the list before registering again'
144
+ : 'The identity service could not be reached',
145
+ 0,
146
+ );
147
+ }
148
+
149
+ let json: any = null;
150
+ try { json = await res.json(); } catch { json = null; }
151
+
152
+ if (!res.ok) {
153
+ const e = json?.error;
154
+ throw new AgentRegistryError(
155
+ typeof e?.code === 'string' ? e.code : `HTTP_${res.status}`,
156
+ typeof e?.message === 'string' ? e.message : `The identity service answered ${res.status}`,
157
+ res.status,
158
+ { requestId: json?.request_id ?? e?.support?.request_id, details: e?.details },
159
+ );
160
+ }
161
+
162
+ const data = json?.data;
163
+ if (json?.success === false || data == null || typeof data !== 'object') {
164
+ throw new AgentRegistryError('BAD_RESPONSE', 'The identity service answered without the expected data', res.status);
165
+ }
166
+ return data as T;
167
+ }
168
+
169
+ return {
170
+ register: (input) =>
171
+ call<RegisterAgentResult>('/api/client/agents', {
172
+ method: 'POST',
173
+ // Exactly the three fields the door binds; nothing else is forwarded.
174
+ body: { user_name: input.user_name, owner_user_id: input.owner_user_id, host_class: input.host_class },
175
+ }),
176
+ list: () => call<ListAgentsResult>('/api/client/agents', { method: 'GET' }),
177
+ get: (id) => call<GetAgentResult>(`/api/client/agents/${encodeURIComponent(String(id))}`, { method: 'GET' }),
178
+ };
179
+ }
@@ -5,6 +5,7 @@ import { TraditionalAuthSection } from './TraditionalAuthSection';
5
5
  import { safeCallbackPath } from '../../auth/safe-callback';
6
6
  import { authorizeResumeHref } from '../../lib/authorize-resume';
7
7
  import { NO_CODE_REQUEST, codeStepInstruction, type CodeRequest } from '../../lib/code-step-copy';
8
+ import { DevCodeNotice } from './DevCodeNotice';
8
9
  import {
9
10
  credentialSignIn,
10
11
  requestEmailCode,
@@ -73,6 +74,7 @@ export function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showF
73
74
  const [fieldError, setFieldError] = useState<string | null>(null);
74
75
  const [codeRequest, setCodeRequest] = useState<CodeRequest>(NO_CODE_REQUEST);
75
76
  const [offerEmailCode, setOfferEmailCode] = useState(false);
77
+ const [devCode, setDevCode] = useState<string | null>(null);
76
78
  const [emailCodeMsg, setEmailCodeMsg] = useState<string | null>(null);
77
79
 
78
80
  async function submit(withCode?: string) {
@@ -89,6 +91,7 @@ export function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showF
89
91
  }
90
92
  if (r.kind === 'two_factor') {
91
93
  setCodeRequest(r.codeRequest);
94
+ setDevCode(r.devCode);
92
95
  setStep('code');
93
96
  return;
94
97
  }
@@ -124,6 +127,7 @@ export function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showF
124
127
  function onBack() {
125
128
  setStep('credentials');
126
129
  setCodeRequest(NO_CODE_REQUEST);
130
+ setDevCode(null);
127
131
  setCode('');
128
132
  setFieldError(null);
129
133
  setError(null);
@@ -138,6 +142,7 @@ export function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showF
138
142
  const r = await requestEmailCode(fetchImpl);
139
143
  if (r.kind === 'requested') {
140
144
  setCodeRequest(r.codeRequest);
145
+ setDevCode(r.devCode);
141
146
  setError(null);
142
147
  setOfferEmailCode(false);
143
148
  setEmailCodeMsg('We asked for a code to be emailed to you. Enter it above when it arrives.');
@@ -182,6 +187,7 @@ export function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showF
182
187
  className="w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50"
183
188
  style={inputStyle}
184
189
  />
190
+ <DevCodeNotice code={devCode} />
185
191
  </div>
186
192
  <button
187
193
  type="submit"
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * AUTH-045 (BAPert 75627): on a DEV identity service that echoes the emailed sign-in code in its own answer (Email:DevCodeEcho, an
5
+ * IdP setting that is on only in Development), the sign-in form shows that code under the code box, because on a dev rig the email
6
+ * goes to nobody. This component is a PURE PASSTHROUGH of the server's `devVerificationCode`: it renders nothing unless it is given
7
+ * a non-empty string, and CredentialSignInForm gives it nothing but the value the door returned. There is no environment flag in the
8
+ * app, so a production IdP (echo off, field absent) can never make it appear.
9
+ */
10
+ export function DevCodeNotice({ code }: { code?: string | null }) {
11
+ if (typeof code !== 'string' || code === '') return null;
12
+ return (
13
+ <p data-testid="dev-sign-in-code" className="text-sm mt-3" style={{ color: 'var(--text-muted, #6b7280)' }}>
14
+ Development sign-in code: <strong>{code}</strong>
15
+ </p>
16
+ );
17
+ }
@@ -22,7 +22,7 @@ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>
22
22
 
23
23
  export type CredentialSignInResult =
24
24
  | { kind: 'success' }
25
- | { kind: 'two_factor'; codeRequest: CodeRequest }
25
+ | { kind: 'two_factor'; codeRequest: CodeRequest; devCode: string | null }
26
26
  | { kind: 'rate_limited'; message: string }
27
27
  | { kind: 'error'; message: string; offerEmailCode: boolean };
28
28
 
@@ -46,6 +46,11 @@ export function validateCredentials(email: string, password: string): { email?:
46
46
  return errors.email || errors.password ? errors : null;
47
47
  }
48
48
 
49
+ /** The dev echo: the door passes the IdP's DevVerificationCode through as `devVerificationCode`. Nothing else is ever a dev code. */
50
+ function readDevCode(body: any): string | null {
51
+ return typeof body?.devVerificationCode === 'string' && body.devVerificationCode ? body.devVerificationCode : null;
52
+ }
53
+
49
54
  function asString(v: unknown): string | undefined {
50
55
  return typeof v === 'string' && v ? v : undefined;
51
56
  }
@@ -64,7 +69,7 @@ export async function credentialSignIn(
64
69
  const body: any = await res.json().catch(() => null);
65
70
 
66
71
  if (res.status === 429) return { kind: 'rate_limited', message: asString(body?.error) ?? MSG_RATE_LIMITED };
67
- if (body?.requiresTwoFactor) return { kind: 'two_factor', codeRequest: readCodeRequest(body) };
72
+ if (body?.requiresTwoFactor) return { kind: 'two_factor', codeRequest: readCodeRequest(body), devCode: readDevCode(body) };
68
73
  if (!res.ok || body?.success !== true) {
69
74
  return { kind: 'error', message: asString(body?.error) ?? MSG_CANNOT_SIGN_IN, offerEmailCode: body?.offerEmailCode === true };
70
75
  }
@@ -89,7 +94,7 @@ export async function requestEmailCode(fetchImpl: FetchLike = (input, init) => f
89
94
  return {
90
95
  kind: 'requested',
91
96
  codeRequest: readCodeRequest(body),
92
- devCode: typeof body?.devVerificationCode === 'string' && body.devVerificationCode ? body.devVerificationCode : null,
97
+ devCode: readDevCode(body),
93
98
  };
94
99
  }
95
100
  return { kind: 'error', message: asString(body?.error) ?? MSG_EMAIL_CODE_FAILED };