@payez/next-mvp 4.8.3 → 4.8.4
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/package.json +6 -1
- package/src/agents/index.ts +1 -0
- package/src/agents/registry.ts +179 -0
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.4",
|
|
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
|
+
}
|