@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 +27 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.js +17 -0
- package/dist/agents/registry.d.ts +73 -0
- package/dist/agents/registry.js +100 -0
- package/dist/components/auth/CredentialSignInForm.js +6 -1
- package/dist/components/auth/DevCodeNotice.d.ts +10 -0
- package/dist/components/auth/DevCodeNotice.js +16 -0
- package/dist/lib/credential-sign-in-client.d.ts +1 -0
- package/dist/lib/credential-sign-in-client.js +6 -2
- package/package.json +6 -1
- package/src/agents/index.ts +1 -0
- package/src/agents/registry.ts +179 -0
- package/src/components/auth/CredentialSignInForm.tsx +6 -0
- package/src/components/auth/DevCodeNotice.tsx +17 -0
- package/src/lib/credential-sign-in-client.ts +8 -3
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
|
+
}
|
|
@@ -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:
|
|
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
|
+
"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:
|
|
97
|
+
devCode: readDevCode(body),
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
100
|
return { kind: 'error', message: asString(body?.error) ?? MSG_EMAIL_CODE_FAILED };
|