@robosystems/client 0.3.0 → 0.3.1
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/extensions/InvestorClient.d.ts +7 -0
- package/extensions/InvestorClient.ts +7 -0
- package/extensions/LedgerClient.d.ts +7 -0
- package/extensions/LedgerClient.ts +7 -0
- package/extensions/ReportClient.d.ts +7 -0
- package/extensions/ReportClient.ts +7 -0
- package/extensions/config.d.ts +19 -0
- package/extensions/config.ts +21 -1
- package/extensions/graphql/client.d.ts +37 -1
- package/extensions/graphql/client.js +81 -7
- package/extensions/graphql/client.ts +114 -7
- package/extensions/index.d.ts +13 -0
- package/extensions/index.js +13 -0
- package/extensions/index.ts +35 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CreatePortfolioRequest, CreatePositionRequest, CreateSecurityRequest, UpdatePortfolioOperation, UpdatePositionOperation, UpdateSecurityOperation } from '../types.gen';
|
|
2
|
+
import type { TokenProvider } from './graphql/client';
|
|
2
3
|
import { type GetInvestorHoldingsQuery, type GetInvestorPortfolioQuery, type GetInvestorPositionQuery, type GetInvestorSecurityQuery, type ListInvestorPortfoliosQuery, type ListInvestorPositionsQuery, type ListInvestorSecuritiesQuery } from './graphql/generated/graphql';
|
|
3
4
|
export type InvestorPortfolioList = NonNullable<ListInvestorPortfoliosQuery['portfolios']>;
|
|
4
5
|
export type InvestorPortfolioSummary = InvestorPortfolioList['portfolios'][number];
|
|
@@ -16,7 +17,13 @@ interface InvestorClientConfig {
|
|
|
16
17
|
baseUrl: string;
|
|
17
18
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
18
19
|
headers?: Record<string, string>;
|
|
20
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
19
21
|
token?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
24
|
+
* request so refreshes flow through automatically.
|
|
25
|
+
*/
|
|
26
|
+
tokenProvider?: TokenProvider;
|
|
20
27
|
}
|
|
21
28
|
export declare class InvestorClient {
|
|
22
29
|
private config;
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
UpdatePositionOperation,
|
|
39
39
|
UpdateSecurityOperation,
|
|
40
40
|
} from '../types.gen'
|
|
41
|
+
import type { TokenProvider } from './graphql/client'
|
|
41
42
|
import { GraphQLClientCache } from './graphql/client'
|
|
42
43
|
import {
|
|
43
44
|
GetInvestorHoldingsDocument,
|
|
@@ -80,7 +81,13 @@ interface InvestorClientConfig {
|
|
|
80
81
|
baseUrl: string
|
|
81
82
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
82
83
|
headers?: Record<string, string>
|
|
84
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
83
85
|
token?: string
|
|
86
|
+
/**
|
|
87
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
88
|
+
* request so refreshes flow through automatically.
|
|
89
|
+
*/
|
|
90
|
+
tokenProvider?: TokenProvider
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
export class InvestorClient {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AutoMapElementsOperation, CreateMappingAssociationOperation, CreateStructureRequest, CreateTaxonomyRequest, DeleteMappingAssociationOperation, OperationEnvelope, UpdateEntityRequest } from '../types.gen';
|
|
2
|
+
import type { TokenProvider } from './graphql/client';
|
|
2
3
|
import { type GetLedgerAccountRollupsQuery, type GetLedgerAccountTreeQuery, type GetLedgerClosingBookStructuresQuery, type GetLedgerEntityQuery, type GetLedgerFiscalCalendarQuery, type GetLedgerMappedTrialBalanceQuery, type GetLedgerMappingCoverageQuery, type GetLedgerMappingQuery, type GetLedgerPeriodCloseStatusQuery, type GetLedgerPeriodDraftsQuery, type GetLedgerReportingTaxonomyQuery, type GetLedgerScheduleFactsQuery, type GetLedgerSummaryQuery, type GetLedgerTransactionQuery, type GetLedgerTrialBalanceQuery, type ListLedgerAccountsQuery, type ListLedgerElementsQuery, type ListLedgerEntitiesQuery, type ListLedgerMappingsQuery, type ListLedgerSchedulesQuery, type ListLedgerStructuresQuery, type ListLedgerTaxonomiesQuery, type ListLedgerTransactionsQuery, type ListLedgerUnmappedElementsQuery } from './graphql/generated/graphql';
|
|
3
4
|
export type LedgerEntity = NonNullable<GetLedgerEntityQuery['entity']>;
|
|
4
5
|
export type LedgerEntitySummary = ListLedgerEntitiesQuery['entities'][number];
|
|
@@ -125,7 +126,13 @@ interface LedgerClientConfig {
|
|
|
125
126
|
baseUrl: string;
|
|
126
127
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
127
128
|
headers?: Record<string, string>;
|
|
129
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
128
130
|
token?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
133
|
+
* request so refreshes flow through automatically.
|
|
134
|
+
*/
|
|
135
|
+
tokenProvider?: TokenProvider;
|
|
129
136
|
}
|
|
130
137
|
export declare class LedgerClient {
|
|
131
138
|
private config;
|
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
TruncateScheduleOperation,
|
|
58
58
|
UpdateEntityRequest,
|
|
59
59
|
} from '../types.gen'
|
|
60
|
+
import type { TokenProvider } from './graphql/client'
|
|
60
61
|
import { GraphQLClientCache } from './graphql/client'
|
|
61
62
|
import {
|
|
62
63
|
GetLedgerAccountRollupsDocument,
|
|
@@ -347,7 +348,13 @@ interface LedgerClientConfig {
|
|
|
347
348
|
baseUrl: string
|
|
348
349
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
349
350
|
headers?: Record<string, string>
|
|
351
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
350
352
|
token?: string
|
|
353
|
+
/**
|
|
354
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
355
|
+
* request so refreshes flow through automatically.
|
|
356
|
+
*/
|
|
357
|
+
tokenProvider?: TokenProvider
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
export class LedgerClient {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OperationEnvelope } from '../types.gen';
|
|
2
|
+
import type { TokenProvider } from './graphql/client';
|
|
2
3
|
import { type GetLedgerPublishListQuery, type GetLedgerReportQuery, type GetLedgerStatementQuery, type ListLedgerPublishListsQuery, type ListLedgerReportsQuery } from './graphql/generated/graphql';
|
|
3
4
|
export type Report = NonNullable<GetLedgerReportQuery['report']>;
|
|
4
5
|
export type ReportListItem = NonNullable<ListLedgerReportsQuery['reports']>['reports'][number];
|
|
@@ -50,7 +51,13 @@ interface ReportClientConfig {
|
|
|
50
51
|
baseUrl: string;
|
|
51
52
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
52
53
|
headers?: Record<string, string>;
|
|
54
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
53
55
|
token?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
58
|
+
* request so refreshes flow through automatically.
|
|
59
|
+
*/
|
|
60
|
+
tokenProvider?: TokenProvider;
|
|
54
61
|
}
|
|
55
62
|
export declare class ReportClient {
|
|
56
63
|
private config;
|
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
OperationEnvelope,
|
|
43
43
|
UpdatePublishListOperation,
|
|
44
44
|
} from '../types.gen'
|
|
45
|
+
import type { TokenProvider } from './graphql/client'
|
|
45
46
|
import { GraphQLClientCache } from './graphql/client'
|
|
46
47
|
import {
|
|
47
48
|
GetLedgerPublishListDocument,
|
|
@@ -119,7 +120,13 @@ interface ReportClientConfig {
|
|
|
119
120
|
baseUrl: string
|
|
120
121
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
121
122
|
headers?: Record<string, string>
|
|
123
|
+
/** Static credential — use `tokenProvider` instead if the JWT rotates. */
|
|
122
124
|
token?: string
|
|
125
|
+
/**
|
|
126
|
+
* Dynamic credential callback. When set, invoked on every GraphQL
|
|
127
|
+
* request so refreshes flow through automatically.
|
|
128
|
+
*/
|
|
129
|
+
tokenProvider?: TokenProvider
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
export class ReportClient {
|
package/extensions/config.d.ts
CHANGED
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
* Configuration for SDK extensions
|
|
3
3
|
* Provides centralized configuration for CORS, credentials, and other settings
|
|
4
4
|
*/
|
|
5
|
+
import type { TokenProvider } from './graphql/client';
|
|
5
6
|
export interface SDKExtensionsConfig {
|
|
6
7
|
baseUrl?: string;
|
|
7
8
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
8
9
|
headers?: Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Static credential captured at singleton construction. Fine for
|
|
12
|
+
* long-lived API keys — use `tokenProvider` instead when the
|
|
13
|
+
* JWT can rotate mid-session (browser login flows).
|
|
14
|
+
*
|
|
15
|
+
* When both `token` and `tokenProvider` are set, `tokenProvider`
|
|
16
|
+
* wins — the static value is effectively ignored. Pass
|
|
17
|
+
* `tokenProvider: undefined` to explicitly switch back to the
|
|
18
|
+
* static-token path.
|
|
19
|
+
*/
|
|
9
20
|
token?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Dynamic credential callback, invoked on every GraphQL request.
|
|
23
|
+
* When set, JWT refresh flows through automatically: the lazy
|
|
24
|
+
* `extensions` singleton picks it up at construction, and each
|
|
25
|
+
* request consults the callback for the current token instead of
|
|
26
|
+
* reusing a stale captured value.
|
|
27
|
+
*/
|
|
28
|
+
tokenProvider?: TokenProvider;
|
|
10
29
|
timeout?: number;
|
|
11
30
|
maxRetries?: number;
|
|
12
31
|
retryDelay?: number;
|
package/extensions/config.ts
CHANGED
|
@@ -5,11 +5,31 @@
|
|
|
5
5
|
* Provides centralized configuration for CORS, credentials, and other settings
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { TokenProvider } from './graphql/client'
|
|
9
|
+
|
|
8
10
|
export interface SDKExtensionsConfig {
|
|
9
11
|
baseUrl?: string
|
|
10
12
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
11
13
|
headers?: Record<string, string>
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Static credential captured at singleton construction. Fine for
|
|
16
|
+
* long-lived API keys — use `tokenProvider` instead when the
|
|
17
|
+
* JWT can rotate mid-session (browser login flows).
|
|
18
|
+
*
|
|
19
|
+
* When both `token` and `tokenProvider` are set, `tokenProvider`
|
|
20
|
+
* wins — the static value is effectively ignored. Pass
|
|
21
|
+
* `tokenProvider: undefined` to explicitly switch back to the
|
|
22
|
+
* static-token path.
|
|
23
|
+
*/
|
|
24
|
+
token?: string
|
|
25
|
+
/**
|
|
26
|
+
* Dynamic credential callback, invoked on every GraphQL request.
|
|
27
|
+
* When set, JWT refresh flows through automatically: the lazy
|
|
28
|
+
* `extensions` singleton picks it up at construction, and each
|
|
29
|
+
* request consults the callback for the current token instead of
|
|
30
|
+
* reusing a stale captured value.
|
|
31
|
+
*/
|
|
32
|
+
tokenProvider?: TokenProvider
|
|
13
33
|
timeout?: number
|
|
14
34
|
maxRetries?: number
|
|
15
35
|
retryDelay?: number
|
|
@@ -19,9 +19,36 @@
|
|
|
19
19
|
* from the query files in sdk-extensions/graphql/queries/.
|
|
20
20
|
*/
|
|
21
21
|
import { GraphQLClient } from 'graphql-request';
|
|
22
|
+
/**
|
|
23
|
+
* Callback that returns the current auth credential on demand.
|
|
24
|
+
*
|
|
25
|
+
* Use this instead of the static `token` field when the credential
|
|
26
|
+
* can rotate during the lifetime of the client — the primary case
|
|
27
|
+
* is short-lived JWTs that auto-refresh from browser token storage.
|
|
28
|
+
* The provider is invoked on **every** GraphQL request, so the
|
|
29
|
+
* returned value should be cheap to obtain (a localStorage read or
|
|
30
|
+
* an in-memory lookup, not a network call if avoidable).
|
|
31
|
+
*
|
|
32
|
+
* May return `null`/`undefined` to indicate "no credential available
|
|
33
|
+
* right now" — the request will be sent unauthenticated in that
|
|
34
|
+
* case, which lets the caller distinguish auth-expired errors from
|
|
35
|
+
* never-authed errors at the transport layer.
|
|
36
|
+
*/
|
|
37
|
+
export type TokenProvider = () => string | null | undefined | Promise<string | null | undefined>;
|
|
22
38
|
export interface GraphQLClientConfig {
|
|
23
39
|
baseUrl: string;
|
|
40
|
+
/**
|
|
41
|
+
* Static credential captured at construction time. Use this when
|
|
42
|
+
* the token won't rotate (e.g. CLI/server flows using a long-lived
|
|
43
|
+
* API key). For JWT flows prefer `tokenProvider` so refreshes are
|
|
44
|
+
* picked up without rebuilding the client.
|
|
45
|
+
*/
|
|
24
46
|
token?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Dynamic credential callback, read on every request. Wins over
|
|
49
|
+
* `token` when both are set. See `TokenProvider` for semantics.
|
|
50
|
+
*/
|
|
51
|
+
tokenProvider?: TokenProvider;
|
|
25
52
|
headers?: Record<string, string>;
|
|
26
53
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
27
54
|
}
|
|
@@ -41,6 +68,15 @@ export declare class GraphQLClientCache {
|
|
|
41
68
|
private clients;
|
|
42
69
|
constructor(config: GraphQLClientConfig);
|
|
43
70
|
get(graphId: string): GraphQLClient;
|
|
44
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Drop all cached clients. Only needed when swapping **static**
|
|
73
|
+
* credentials — e.g. replacing the `token` field on a long-lived
|
|
74
|
+
* facade between tenants, or resetting a CLI session. When the
|
|
75
|
+
* cache was built from a `tokenProvider`, rotation is handled
|
|
76
|
+
* per-request inside `requestMiddleware` and `clear()` is a no-op
|
|
77
|
+
* for auth purposes (the cached `GraphQLClient` instances keep
|
|
78
|
+
* using the same provider reference and will pick up the next
|
|
79
|
+
* token automatically).
|
|
80
|
+
*/
|
|
45
81
|
clear(): void;
|
|
46
82
|
}
|
|
@@ -24,6 +24,26 @@ exports.createGraphQLClient = createGraphQLClient;
|
|
|
24
24
|
* from the query files in sdk-extensions/graphql/queries/.
|
|
25
25
|
*/
|
|
26
26
|
const graphql_request_1 = require("graphql-request");
|
|
27
|
+
/**
|
|
28
|
+
* Apply a credential to an in-progress request's headers, choosing
|
|
29
|
+
* the right header based on token shape:
|
|
30
|
+
*
|
|
31
|
+
* - `rfs…` prefix → `X-API-Key` (long-lived API key, validated
|
|
32
|
+
* against the database's api_keys table).
|
|
33
|
+
* - anything else → `Authorization: Bearer …` (short-lived JWT,
|
|
34
|
+
* validated by JWT middleware).
|
|
35
|
+
*
|
|
36
|
+
* The two credential types are NOT interchangeable at the backend —
|
|
37
|
+
* sending a JWT as `X-API-Key` or an API key as Bearer both 401.
|
|
38
|
+
*/
|
|
39
|
+
function applyAuthHeader(headers, token) {
|
|
40
|
+
if (token.startsWith('rfs')) {
|
|
41
|
+
headers.set('X-API-Key', token);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
27
47
|
/**
|
|
28
48
|
* Build a new GraphQL client for the given graph. Prefer
|
|
29
49
|
* `GraphQLClientCache.get(graphId)` in facade code — this is the raw
|
|
@@ -34,17 +54,62 @@ function createGraphQLClient(config, graphId) {
|
|
|
34
54
|
throw new Error('createGraphQLClient requires a non-empty graphId');
|
|
35
55
|
}
|
|
36
56
|
const url = `${config.baseUrl.replace(/\/$/, '')}/extensions/${graphId}/graphql`;
|
|
37
|
-
const
|
|
57
|
+
const staticHeaders = {
|
|
38
58
|
...(config.headers ?? {}),
|
|
39
59
|
};
|
|
60
|
+
// Dynamic-token path: defer credential injection to a per-request
|
|
61
|
+
// middleware so JWT refreshes are picked up without rebuilding or
|
|
62
|
+
// clearing the client. This is the recommended path for browser
|
|
63
|
+
// flows where the token in localStorage rotates every ~30 minutes.
|
|
64
|
+
if (config.tokenProvider) {
|
|
65
|
+
const providerFn = config.tokenProvider;
|
|
66
|
+
return new graphql_request_1.GraphQLClient(url, {
|
|
67
|
+
headers: staticHeaders,
|
|
68
|
+
credentials: config.credentials,
|
|
69
|
+
requestMiddleware: async (request) => {
|
|
70
|
+
let token;
|
|
71
|
+
try {
|
|
72
|
+
token = await providerFn();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// A provider failure shouldn't crash the request — fall
|
|
76
|
+
// through unauthenticated so the backend returns a clean
|
|
77
|
+
// 401, which is easier to diagnose than a thrown middleware.
|
|
78
|
+
// We still log a breadcrumb so the failure is visible in
|
|
79
|
+
// devtools/log aggregators instead of silently disappearing;
|
|
80
|
+
// silently swallowing provider bugs in production is worse
|
|
81
|
+
// than the noise.
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.warn('[RoboSystems SDK] tokenProvider threw — sending unauthenticated request:', err);
|
|
84
|
+
token = undefined;
|
|
85
|
+
}
|
|
86
|
+
if (!token) {
|
|
87
|
+
return request;
|
|
88
|
+
}
|
|
89
|
+
// `request.headers` is a HeadersInit (Headers | string[][] |
|
|
90
|
+
// Record<string, string>), so normalize via Headers before
|
|
91
|
+
// mutating. Spreading it directly loses keys when it's a
|
|
92
|
+
// Headers instance.
|
|
93
|
+
const merged = new Headers(request.headers);
|
|
94
|
+
applyAuthHeader(merged, token);
|
|
95
|
+
return { ...request, headers: merged };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Static-token path: pick the right header at construction time.
|
|
100
|
+
// Suitable for long-lived API keys that never rotate.
|
|
40
101
|
if (config.token) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
102
|
+
const headers = new Headers(staticHeaders);
|
|
103
|
+
applyAuthHeader(headers, config.token);
|
|
104
|
+
return new graphql_request_1.GraphQLClient(url, {
|
|
105
|
+
headers,
|
|
106
|
+
credentials: config.credentials,
|
|
107
|
+
});
|
|
45
108
|
}
|
|
109
|
+
// No credentials at all — used by unauthenticated introspection
|
|
110
|
+
// queries against public dev endpoints.
|
|
46
111
|
return new graphql_request_1.GraphQLClient(url, {
|
|
47
|
-
headers,
|
|
112
|
+
headers: staticHeaders,
|
|
48
113
|
credentials: config.credentials,
|
|
49
114
|
});
|
|
50
115
|
}
|
|
@@ -67,7 +132,16 @@ class GraphQLClientCache {
|
|
|
67
132
|
this.clients.set(graphId, client);
|
|
68
133
|
return client;
|
|
69
134
|
}
|
|
70
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Drop all cached clients. Only needed when swapping **static**
|
|
137
|
+
* credentials — e.g. replacing the `token` field on a long-lived
|
|
138
|
+
* facade between tenants, or resetting a CLI session. When the
|
|
139
|
+
* cache was built from a `tokenProvider`, rotation is handled
|
|
140
|
+
* per-request inside `requestMiddleware` and `clear()` is a no-op
|
|
141
|
+
* for auth purposes (the cached `GraphQLClient` instances keep
|
|
142
|
+
* using the same provider reference and will pick up the next
|
|
143
|
+
* token automatically).
|
|
144
|
+
*/
|
|
71
145
|
clear() {
|
|
72
146
|
this.clients.clear();
|
|
73
147
|
}
|
|
@@ -23,13 +23,61 @@
|
|
|
23
23
|
|
|
24
24
|
import { GraphQLClient } from 'graphql-request'
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Callback that returns the current auth credential on demand.
|
|
28
|
+
*
|
|
29
|
+
* Use this instead of the static `token` field when the credential
|
|
30
|
+
* can rotate during the lifetime of the client — the primary case
|
|
31
|
+
* is short-lived JWTs that auto-refresh from browser token storage.
|
|
32
|
+
* The provider is invoked on **every** GraphQL request, so the
|
|
33
|
+
* returned value should be cheap to obtain (a localStorage read or
|
|
34
|
+
* an in-memory lookup, not a network call if avoidable).
|
|
35
|
+
*
|
|
36
|
+
* May return `null`/`undefined` to indicate "no credential available
|
|
37
|
+
* right now" — the request will be sent unauthenticated in that
|
|
38
|
+
* case, which lets the caller distinguish auth-expired errors from
|
|
39
|
+
* never-authed errors at the transport layer.
|
|
40
|
+
*/
|
|
41
|
+
export type TokenProvider = () => string | null | undefined | Promise<string | null | undefined>
|
|
42
|
+
|
|
26
43
|
export interface GraphQLClientConfig {
|
|
27
44
|
baseUrl: string
|
|
45
|
+
/**
|
|
46
|
+
* Static credential captured at construction time. Use this when
|
|
47
|
+
* the token won't rotate (e.g. CLI/server flows using a long-lived
|
|
48
|
+
* API key). For JWT flows prefer `tokenProvider` so refreshes are
|
|
49
|
+
* picked up without rebuilding the client.
|
|
50
|
+
*/
|
|
28
51
|
token?: string
|
|
52
|
+
/**
|
|
53
|
+
* Dynamic credential callback, read on every request. Wins over
|
|
54
|
+
* `token` when both are set. See `TokenProvider` for semantics.
|
|
55
|
+
*/
|
|
56
|
+
tokenProvider?: TokenProvider
|
|
29
57
|
headers?: Record<string, string>
|
|
30
58
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
31
59
|
}
|
|
32
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Apply a credential to an in-progress request's headers, choosing
|
|
63
|
+
* the right header based on token shape:
|
|
64
|
+
*
|
|
65
|
+
* - `rfs…` prefix → `X-API-Key` (long-lived API key, validated
|
|
66
|
+
* against the database's api_keys table).
|
|
67
|
+
* - anything else → `Authorization: Bearer …` (short-lived JWT,
|
|
68
|
+
* validated by JWT middleware).
|
|
69
|
+
*
|
|
70
|
+
* The two credential types are NOT interchangeable at the backend —
|
|
71
|
+
* sending a JWT as `X-API-Key` or an API key as Bearer both 401.
|
|
72
|
+
*/
|
|
73
|
+
function applyAuthHeader(headers: Headers, token: string): void {
|
|
74
|
+
if (token.startsWith('rfs')) {
|
|
75
|
+
headers.set('X-API-Key', token)
|
|
76
|
+
} else {
|
|
77
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
33
81
|
/**
|
|
34
82
|
* Build a new GraphQL client for the given graph. Prefer
|
|
35
83
|
* `GraphQLClientCache.get(graphId)` in facade code — this is the raw
|
|
@@ -40,17 +88,67 @@ export function createGraphQLClient(config: GraphQLClientConfig, graphId: string
|
|
|
40
88
|
throw new Error('createGraphQLClient requires a non-empty graphId')
|
|
41
89
|
}
|
|
42
90
|
const url = `${config.baseUrl.replace(/\/$/, '')}/extensions/${graphId}/graphql`
|
|
43
|
-
const
|
|
91
|
+
const staticHeaders: Record<string, string> = {
|
|
44
92
|
...(config.headers ?? {}),
|
|
45
93
|
}
|
|
94
|
+
|
|
95
|
+
// Dynamic-token path: defer credential injection to a per-request
|
|
96
|
+
// middleware so JWT refreshes are picked up without rebuilding or
|
|
97
|
+
// clearing the client. This is the recommended path for browser
|
|
98
|
+
// flows where the token in localStorage rotates every ~30 minutes.
|
|
99
|
+
if (config.tokenProvider) {
|
|
100
|
+
const providerFn = config.tokenProvider
|
|
101
|
+
return new GraphQLClient(url, {
|
|
102
|
+
headers: staticHeaders,
|
|
103
|
+
credentials: config.credentials,
|
|
104
|
+
requestMiddleware: async (request) => {
|
|
105
|
+
let token: string | null | undefined
|
|
106
|
+
try {
|
|
107
|
+
token = await providerFn()
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// A provider failure shouldn't crash the request — fall
|
|
110
|
+
// through unauthenticated so the backend returns a clean
|
|
111
|
+
// 401, which is easier to diagnose than a thrown middleware.
|
|
112
|
+
// We still log a breadcrumb so the failure is visible in
|
|
113
|
+
// devtools/log aggregators instead of silently disappearing;
|
|
114
|
+
// silently swallowing provider bugs in production is worse
|
|
115
|
+
// than the noise.
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.warn(
|
|
118
|
+
'[RoboSystems SDK] tokenProvider threw — sending unauthenticated request:',
|
|
119
|
+
err
|
|
120
|
+
)
|
|
121
|
+
token = undefined
|
|
122
|
+
}
|
|
123
|
+
if (!token) {
|
|
124
|
+
return request
|
|
125
|
+
}
|
|
126
|
+
// `request.headers` is a HeadersInit (Headers | string[][] |
|
|
127
|
+
// Record<string, string>), so normalize via Headers before
|
|
128
|
+
// mutating. Spreading it directly loses keys when it's a
|
|
129
|
+
// Headers instance.
|
|
130
|
+
const merged = new Headers(request.headers as HeadersInit | undefined)
|
|
131
|
+
applyAuthHeader(merged, token)
|
|
132
|
+
return { ...request, headers: merged }
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Static-token path: pick the right header at construction time.
|
|
138
|
+
// Suitable for long-lived API keys that never rotate.
|
|
46
139
|
if (config.token) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
140
|
+
const headers = new Headers(staticHeaders)
|
|
141
|
+
applyAuthHeader(headers, config.token)
|
|
142
|
+
return new GraphQLClient(url, {
|
|
143
|
+
headers,
|
|
144
|
+
credentials: config.credentials,
|
|
145
|
+
})
|
|
51
146
|
}
|
|
147
|
+
|
|
148
|
+
// No credentials at all — used by unauthenticated introspection
|
|
149
|
+
// queries against public dev endpoints.
|
|
52
150
|
return new GraphQLClient(url, {
|
|
53
|
-
headers,
|
|
151
|
+
headers: staticHeaders,
|
|
54
152
|
credentials: config.credentials,
|
|
55
153
|
})
|
|
56
154
|
}
|
|
@@ -75,7 +173,16 @@ export class GraphQLClientCache {
|
|
|
75
173
|
return client
|
|
76
174
|
}
|
|
77
175
|
|
|
78
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Drop all cached clients. Only needed when swapping **static**
|
|
178
|
+
* credentials — e.g. replacing the `token` field on a long-lived
|
|
179
|
+
* facade between tenants, or resetting a CLI session. When the
|
|
180
|
+
* cache was built from a `tokenProvider`, rotation is handled
|
|
181
|
+
* per-request inside `requestMiddleware` and `clear()` is a no-op
|
|
182
|
+
* for auth purposes (the cached `GraphQLClient` instances keep
|
|
183
|
+
* using the same provider reference and will pick up the next
|
|
184
|
+
* token automatically).
|
|
185
|
+
*/
|
|
79
186
|
clear(): void {
|
|
80
187
|
this.clients.clear()
|
|
81
188
|
}
|
package/extensions/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* RoboSystems SDK Extensions
|
|
3
3
|
* Enhanced clients with SSE support for the RoboSystems API
|
|
4
4
|
*/
|
|
5
|
+
import type { TokenProvider } from './graphql/client';
|
|
5
6
|
import { OperationClient } from './OperationClient';
|
|
6
7
|
import { QueryClient } from './QueryClient';
|
|
7
8
|
import { AgentClient } from './AgentClient';
|
|
@@ -9,10 +10,22 @@ import { SSEClient } from './SSEClient';
|
|
|
9
10
|
import { InvestorClient } from './InvestorClient';
|
|
10
11
|
import { LedgerClient } from './LedgerClient';
|
|
11
12
|
import { ReportClient } from './ReportClient';
|
|
13
|
+
export type { TokenProvider } from './graphql/client';
|
|
12
14
|
export interface RoboSystemsExtensionConfig {
|
|
13
15
|
baseUrl?: string;
|
|
14
16
|
credentials?: 'include' | 'same-origin' | 'omit';
|
|
17
|
+
/**
|
|
18
|
+
* Static credential captured at construction time. Fine for
|
|
19
|
+
* long-lived API keys; use `tokenProvider` instead when the JWT
|
|
20
|
+
* can rotate (browser login flows).
|
|
21
|
+
*/
|
|
15
22
|
token?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Dynamic credential callback invoked on every GraphQL request.
|
|
25
|
+
* When set, JWT refreshes are picked up automatically — no need
|
|
26
|
+
* to rebuild or clear cached clients after a refresh.
|
|
27
|
+
*/
|
|
28
|
+
tokenProvider?: TokenProvider;
|
|
16
29
|
headers?: Record<string, string>;
|
|
17
30
|
maxRetries?: number;
|
|
18
31
|
retryDelay?: number;
|
package/extensions/index.js
CHANGED
|
@@ -41,10 +41,15 @@ class RoboSystemsExtensions {
|
|
|
41
41
|
const sdkConfig = client_gen_1.client.getConfig();
|
|
42
42
|
// Extract JWT token using centralized logic
|
|
43
43
|
const token = config.token || (0, config_1.extractTokenFromSDKClient)();
|
|
44
|
+
// `tokenProvider` falls back to the global SDK extensions config so
|
|
45
|
+
// apps can wire refresh once via `setSDKExtensionsConfig({ tokenProvider })`
|
|
46
|
+
// and have the lazy default singleton pick it up automatically.
|
|
47
|
+
const tokenProvider = config.tokenProvider || (0, config_1.getSDKExtensionsConfig)().tokenProvider;
|
|
44
48
|
this.config = {
|
|
45
49
|
baseUrl: config.baseUrl || sdkConfig.baseUrl || 'http://localhost:8000',
|
|
46
50
|
credentials: config.credentials || 'include',
|
|
47
51
|
token,
|
|
52
|
+
tokenProvider,
|
|
48
53
|
headers: config.headers,
|
|
49
54
|
maxRetries: config.maxRetries || 5,
|
|
50
55
|
retryDelay: config.retryDelay || 1000,
|
|
@@ -68,22 +73,30 @@ class RoboSystemsExtensions {
|
|
|
68
73
|
maxRetries: this.config.maxRetries,
|
|
69
74
|
retryDelay: this.config.retryDelay,
|
|
70
75
|
});
|
|
76
|
+
// LedgerClient / InvestorClient / ReportClient all use GraphQL
|
|
77
|
+
// internally, so they get the tokenProvider — REST-only clients
|
|
78
|
+
// (QueryClient / AgentClient / OperationClient / SSEClient) do
|
|
79
|
+
// not, because their refresh story lives in the main SDK client
|
|
80
|
+
// wrapper, not here.
|
|
71
81
|
this.ledger = new LedgerClient_1.LedgerClient({
|
|
72
82
|
baseUrl: this.config.baseUrl,
|
|
73
83
|
credentials: this.config.credentials,
|
|
74
84
|
token: this.config.token,
|
|
85
|
+
tokenProvider: this.config.tokenProvider,
|
|
75
86
|
headers: this.config.headers,
|
|
76
87
|
});
|
|
77
88
|
this.investor = new InvestorClient_1.InvestorClient({
|
|
78
89
|
baseUrl: this.config.baseUrl,
|
|
79
90
|
credentials: this.config.credentials,
|
|
80
91
|
token: this.config.token,
|
|
92
|
+
tokenProvider: this.config.tokenProvider,
|
|
81
93
|
headers: this.config.headers,
|
|
82
94
|
});
|
|
83
95
|
this.reports = new ReportClient_1.ReportClient({
|
|
84
96
|
baseUrl: this.config.baseUrl,
|
|
85
97
|
credentials: this.config.credentials,
|
|
86
98
|
token: this.config.token,
|
|
99
|
+
tokenProvider: this.config.tokenProvider,
|
|
87
100
|
headers: this.config.headers,
|
|
88
101
|
});
|
|
89
102
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { client } from '../client.gen'
|
|
7
|
-
import { extractTokenFromSDKClient } from './config'
|
|
7
|
+
import { extractTokenFromSDKClient, getSDKExtensionsConfig } from './config'
|
|
8
|
+
import type { TokenProvider } from './graphql/client'
|
|
8
9
|
import { OperationClient } from './OperationClient'
|
|
9
10
|
import { QueryClient } from './QueryClient'
|
|
10
11
|
import { AgentClient } from './AgentClient'
|
|
@@ -13,10 +14,27 @@ import { InvestorClient } from './InvestorClient'
|
|
|
13
14
|
import { LedgerClient } from './LedgerClient'
|
|
14
15
|
import { ReportClient } from './ReportClient'
|
|
15
16
|
|
|
17
|
+
// Re-export the `TokenProvider` type so consumers who want to type
|
|
18
|
+
// their own callback (`const provider: TokenProvider = () => …`) can
|
|
19
|
+
// pull it from the package root instead of reaching into the
|
|
20
|
+
// internal `./graphql/client` module path.
|
|
21
|
+
export type { TokenProvider } from './graphql/client'
|
|
22
|
+
|
|
16
23
|
export interface RoboSystemsExtensionConfig {
|
|
17
24
|
baseUrl?: string
|
|
18
25
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
19
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Static credential captured at construction time. Fine for
|
|
28
|
+
* long-lived API keys; use `tokenProvider` instead when the JWT
|
|
29
|
+
* can rotate (browser login flows).
|
|
30
|
+
*/
|
|
31
|
+
token?: string
|
|
32
|
+
/**
|
|
33
|
+
* Dynamic credential callback invoked on every GraphQL request.
|
|
34
|
+
* When set, JWT refreshes are picked up automatically — no need
|
|
35
|
+
* to rebuild or clear cached clients after a refresh.
|
|
36
|
+
*/
|
|
37
|
+
tokenProvider?: TokenProvider
|
|
20
38
|
headers?: Record<string, string>
|
|
21
39
|
maxRetries?: number
|
|
22
40
|
retryDelay?: number
|
|
@@ -27,6 +45,7 @@ interface ResolvedConfig {
|
|
|
27
45
|
baseUrl: string
|
|
28
46
|
credentials: 'include' | 'same-origin' | 'omit'
|
|
29
47
|
token?: string
|
|
48
|
+
tokenProvider?: TokenProvider
|
|
30
49
|
headers?: Record<string, string>
|
|
31
50
|
maxRetries: number
|
|
32
51
|
retryDelay: number
|
|
@@ -48,10 +67,16 @@ export class RoboSystemsExtensions {
|
|
|
48
67
|
// Extract JWT token using centralized logic
|
|
49
68
|
const token = config.token || extractTokenFromSDKClient()
|
|
50
69
|
|
|
70
|
+
// `tokenProvider` falls back to the global SDK extensions config so
|
|
71
|
+
// apps can wire refresh once via `setSDKExtensionsConfig({ tokenProvider })`
|
|
72
|
+
// and have the lazy default singleton pick it up automatically.
|
|
73
|
+
const tokenProvider = config.tokenProvider || getSDKExtensionsConfig().tokenProvider
|
|
74
|
+
|
|
51
75
|
this.config = {
|
|
52
76
|
baseUrl: config.baseUrl || sdkConfig.baseUrl || 'http://localhost:8000',
|
|
53
77
|
credentials: config.credentials || 'include',
|
|
54
78
|
token,
|
|
79
|
+
tokenProvider,
|
|
55
80
|
headers: config.headers,
|
|
56
81
|
maxRetries: config.maxRetries || 5,
|
|
57
82
|
retryDelay: config.retryDelay || 1000,
|
|
@@ -79,10 +104,16 @@ export class RoboSystemsExtensions {
|
|
|
79
104
|
retryDelay: this.config.retryDelay,
|
|
80
105
|
})
|
|
81
106
|
|
|
107
|
+
// LedgerClient / InvestorClient / ReportClient all use GraphQL
|
|
108
|
+
// internally, so they get the tokenProvider — REST-only clients
|
|
109
|
+
// (QueryClient / AgentClient / OperationClient / SSEClient) do
|
|
110
|
+
// not, because their refresh story lives in the main SDK client
|
|
111
|
+
// wrapper, not here.
|
|
82
112
|
this.ledger = new LedgerClient({
|
|
83
113
|
baseUrl: this.config.baseUrl,
|
|
84
114
|
credentials: this.config.credentials,
|
|
85
115
|
token: this.config.token,
|
|
116
|
+
tokenProvider: this.config.tokenProvider,
|
|
86
117
|
headers: this.config.headers,
|
|
87
118
|
})
|
|
88
119
|
|
|
@@ -90,6 +121,7 @@ export class RoboSystemsExtensions {
|
|
|
90
121
|
baseUrl: this.config.baseUrl,
|
|
91
122
|
credentials: this.config.credentials,
|
|
92
123
|
token: this.config.token,
|
|
124
|
+
tokenProvider: this.config.tokenProvider,
|
|
93
125
|
headers: this.config.headers,
|
|
94
126
|
})
|
|
95
127
|
|
|
@@ -97,6 +129,7 @@ export class RoboSystemsExtensions {
|
|
|
97
129
|
baseUrl: this.config.baseUrl,
|
|
98
130
|
credentials: this.config.credentials,
|
|
99
131
|
token: this.config.token,
|
|
132
|
+
tokenProvider: this.config.tokenProvider,
|
|
100
133
|
headers: this.config.headers,
|
|
101
134
|
})
|
|
102
135
|
}
|