@enterprisestandard/react 0.0.4 → 0.0.5-beta.20260114.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/dist/group-store.d.ts +164 -0
- package/dist/group-store.d.ts.map +1 -0
- package/dist/iam.d.ts +205 -12
- package/dist/iam.d.ts.map +1 -1
- package/dist/index.d.ts +44 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3164 -572
- package/dist/index.js.map +29 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/session-store.d.ts +179 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/sso.d.ts +74 -16
- package/dist/sso.d.ts.map +1 -1
- package/dist/tenant-server.d.ts +8 -0
- package/dist/tenant-server.d.ts.map +1 -0
- package/dist/tenant.d.ts +280 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/types/base-user.d.ts +27 -0
- package/dist/types/base-user.d.ts.map +1 -0
- package/dist/types/enterprise-user.d.ts +158 -0
- package/dist/types/enterprise-user.d.ts.map +1 -0
- package/dist/{oidc-schema.d.ts → types/oidc-schema.d.ts} +42 -0
- package/dist/types/oidc-schema.d.ts.map +1 -0
- package/dist/types/scim-schema.d.ts +419 -0
- package/dist/types/scim-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/user.d.ts +41 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/workload-schema.d.ts +106 -0
- package/dist/types/workload-schema.d.ts.map +1 -0
- package/dist/ui/sso-provider.d.ts +3 -3
- package/dist/ui/sso-provider.d.ts.map +1 -1
- package/dist/user-store.d.ts +161 -0
- package/dist/user-store.d.ts.map +1 -0
- package/dist/workload-server.d.ts +126 -0
- package/dist/workload-server.d.ts.map +1 -0
- package/dist/workload-token-store.d.ts +187 -0
- package/dist/workload-token-store.d.ts.map +1 -0
- package/dist/workload.d.ts +227 -0
- package/dist/workload.d.ts.map +1 -0
- package/package.json +2 -5
- package/dist/enterprise-user.d.ts +0 -125
- package/dist/enterprise-user.d.ts.map +0 -1
- package/dist/oidc-schema.d.ts.map +0 -1
- package/dist/standard-schema.d.ts.map +0 -1
- /package/dist/{standard-schema.d.ts → types/standard-schema.d.ts} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from './standard-schema';
|
|
2
|
+
/**
|
|
3
|
+
* JWT Assertion Claims for OAuth2 JWT Bearer Grant (RFC 7523) and OAuth2 Access Tokens
|
|
4
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7523
|
|
5
|
+
* @see https://datatracker.ietf.org/doc/html/rfc9068
|
|
6
|
+
*/
|
|
7
|
+
export interface JWTAssertionClaims {
|
|
8
|
+
/**
|
|
9
|
+
* REQUIRED. Issuer - the workload identity (e.g., SPIFFE ID) or authorization server
|
|
10
|
+
*/
|
|
11
|
+
iss: string;
|
|
12
|
+
/**
|
|
13
|
+
* REQUIRED. Subject - the workload identity or service account
|
|
14
|
+
*/
|
|
15
|
+
sub: string;
|
|
16
|
+
/**
|
|
17
|
+
* OPTIONAL. Audience - may be a string or array of strings
|
|
18
|
+
* Note: Required for JWT assertions, but may be absent in OAuth2 access tokens
|
|
19
|
+
*/
|
|
20
|
+
aud?: string | string[];
|
|
21
|
+
/**
|
|
22
|
+
* REQUIRED. Expiration time (Unix timestamp)
|
|
23
|
+
*/
|
|
24
|
+
exp: number;
|
|
25
|
+
/**
|
|
26
|
+
* REQUIRED. Issued at time (Unix timestamp)
|
|
27
|
+
*/
|
|
28
|
+
iat: number;
|
|
29
|
+
/**
|
|
30
|
+
* OPTIONAL. JWT ID - unique identifier for this token
|
|
31
|
+
* Note: Required for JWT assertions, optional for access tokens
|
|
32
|
+
*/
|
|
33
|
+
jti?: string;
|
|
34
|
+
/**
|
|
35
|
+
* OPTIONAL. Requested OAuth scopes (space-delimited)
|
|
36
|
+
*/
|
|
37
|
+
scope?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Allow additional claims for extensibility
|
|
40
|
+
*/
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a StandardSchemaV1 for validating JWT Assertion Claims.
|
|
45
|
+
* @param vendor - The name of the vendor creating this schema
|
|
46
|
+
* @returns A StandardSchemaV1 instance for JWT Assertion Claims validation
|
|
47
|
+
*/
|
|
48
|
+
export declare function jwtAssertionClaimsSchema(vendor: string): StandardSchemaV1<Record<string, unknown>, JWTAssertionClaims>;
|
|
49
|
+
/**
|
|
50
|
+
* Workload Token Response from OAuth2 token endpoint
|
|
51
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
|
|
52
|
+
*/
|
|
53
|
+
export interface WorkloadTokenResponse {
|
|
54
|
+
/**
|
|
55
|
+
* REQUIRED. The access token issued by the authorization server.
|
|
56
|
+
*/
|
|
57
|
+
access_token: string;
|
|
58
|
+
/**
|
|
59
|
+
* REQUIRED. The type of the token (typically "Bearer").
|
|
60
|
+
*/
|
|
61
|
+
token_type: string;
|
|
62
|
+
/**
|
|
63
|
+
* RECOMMENDED. The lifetime in seconds of the access token.
|
|
64
|
+
*/
|
|
65
|
+
expires_in?: number;
|
|
66
|
+
/**
|
|
67
|
+
* OPTIONAL. The scope of the access token.
|
|
68
|
+
*/
|
|
69
|
+
scope?: string;
|
|
70
|
+
/**
|
|
71
|
+
* OPTIONAL. The refresh token (rarely used for workload identities).
|
|
72
|
+
*/
|
|
73
|
+
refresh_token?: string;
|
|
74
|
+
/**
|
|
75
|
+
* OPTIONAL. The expiration time as an ISO 8601 string.
|
|
76
|
+
*/
|
|
77
|
+
expires?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Creates a StandardSchemaV1 for validating Workload Token Responses.
|
|
81
|
+
* @param vendor - The name of the vendor creating this schema
|
|
82
|
+
* @returns A StandardSchemaV1 instance for Workload Token Response validation
|
|
83
|
+
*/
|
|
84
|
+
export declare function workloadTokenResponseSchema(vendor: string): StandardSchemaV1<Record<string, unknown>, WorkloadTokenResponse>;
|
|
85
|
+
/**
|
|
86
|
+
* Token Validation Result
|
|
87
|
+
*/
|
|
88
|
+
export interface TokenValidationResult {
|
|
89
|
+
/**
|
|
90
|
+
* Whether the token is valid
|
|
91
|
+
*/
|
|
92
|
+
valid: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The decoded and validated claims (if valid)
|
|
95
|
+
*/
|
|
96
|
+
claims?: JWTAssertionClaims;
|
|
97
|
+
/**
|
|
98
|
+
* Error message (if invalid)
|
|
99
|
+
*/
|
|
100
|
+
error?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Token expiration time (if valid)
|
|
103
|
+
*/
|
|
104
|
+
expiresAt?: Date;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=workload-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-schema.d.ts","sourceRoot":"","sources":["../../src/types/workload-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,GACb,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,CAAC,CA6F/D;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,GACb,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,CAAC,CA4GlE;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { User } from '../types/user';
|
|
3
3
|
type StorageType = 'local' | 'session' | 'memory';
|
|
4
4
|
interface SSOProviderProps {
|
|
5
5
|
tenantId?: string;
|
|
@@ -12,8 +12,8 @@ interface SSOProviderProps {
|
|
|
12
12
|
children: ReactNode;
|
|
13
13
|
}
|
|
14
14
|
interface SSOContext {
|
|
15
|
-
user:
|
|
16
|
-
setUser: (user:
|
|
15
|
+
user: User | null;
|
|
16
|
+
setUser: (user: User | null) => void;
|
|
17
17
|
isLoading: boolean;
|
|
18
18
|
tokenUrl?: string;
|
|
19
19
|
refreshUrl?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sso-provider.d.ts","sourceRoot":"","sources":["../../src/ui/sso-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAgD,MAAM,OAAO,CAAC;AACpG,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"sso-provider.d.ts","sourceRoot":"","sources":["../../src/ui/sso-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAgD,MAAM,OAAO,CAAC;AACpG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAE1C,KAAK,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,OAAkB,EAClB,UAAU,EACV,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAuB,EACvB,QAAQ,GACT,EAAE,gBAAgB,2CA8JlB;AAED,wBAAgB,OAAO,IAAI,UAAU,CAMpC;AAOD,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,wBAAgB,QAAQ,IAAI,cAAc,CAiGzC;AAED,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6C7F"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User storage for persisting user profiles from SSO authentication.
|
|
3
|
+
*
|
|
4
|
+
* User stores are optional - the package works with JWT cookies alone.
|
|
5
|
+
* User stores are useful when you want to:
|
|
6
|
+
* - Cache user profiles for fast lookup
|
|
7
|
+
* - Store users close to your application (in-memory, Redis, etc.)
|
|
8
|
+
* - Avoid custom IAM/SCIM integration for simple use cases
|
|
9
|
+
*
|
|
10
|
+
* ## When to Use UserStore vs IAM
|
|
11
|
+
*
|
|
12
|
+
* **Use UserStore when:**
|
|
13
|
+
* - You just need fast user lookups without external systems
|
|
14
|
+
* - Users are managed by an external IdP and you just cache them locally
|
|
15
|
+
* - You want simple in-memory or Redis storage
|
|
16
|
+
*
|
|
17
|
+
* **Use IAM (SCIM) when:**
|
|
18
|
+
* - You need to provision users to an external identity provider
|
|
19
|
+
* - You need custom user attributes beyond what SSO provides
|
|
20
|
+
* - You need to sync users with enterprise directories
|
|
21
|
+
*
|
|
22
|
+
* ## Example Usage
|
|
23
|
+
*
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { sso, InMemoryUserStore } from '@enterprisestandard/react/server';
|
|
26
|
+
*
|
|
27
|
+
* const userStore = new InMemoryUserStore();
|
|
28
|
+
*
|
|
29
|
+
* const auth = sso({
|
|
30
|
+
* // ... other config
|
|
31
|
+
* user_store: userStore,
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Later, look up users
|
|
35
|
+
* const user = await userStore.get('user-sub-id');
|
|
36
|
+
* const userByEmail = await userStore.getByEmail('user@example.com');
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
import type { User } from './types/user';
|
|
40
|
+
/**
|
|
41
|
+
* Stored user data with required id and tracking metadata.
|
|
42
|
+
*
|
|
43
|
+
* Extends the SSO User type with:
|
|
44
|
+
* - Required `id` (the `sub` claim from the IdP)
|
|
45
|
+
* - Timestamps for tracking when users were first seen and last updated
|
|
46
|
+
* - Optional custom extended data
|
|
47
|
+
*
|
|
48
|
+
* @template TExtended - Type-safe custom data that consumers can add to users
|
|
49
|
+
*/
|
|
50
|
+
export type StoredUser<TExtended = {}> = User & {
|
|
51
|
+
/**
|
|
52
|
+
* Required unique identifier (the `sub` claim from the IdP).
|
|
53
|
+
* This is the primary key for user storage.
|
|
54
|
+
*/
|
|
55
|
+
id: string;
|
|
56
|
+
/**
|
|
57
|
+
* Timestamp when the user was first stored.
|
|
58
|
+
*/
|
|
59
|
+
createdAt: Date;
|
|
60
|
+
/**
|
|
61
|
+
* Timestamp when the user was last updated (e.g., on re-login).
|
|
62
|
+
*/
|
|
63
|
+
updatedAt: Date;
|
|
64
|
+
} & TExtended;
|
|
65
|
+
/**
|
|
66
|
+
* Abstract interface for user storage backends.
|
|
67
|
+
*
|
|
68
|
+
* Consumers can implement this interface to use different storage backends:
|
|
69
|
+
* - In-memory (for development/testing)
|
|
70
|
+
* - Redis (for production with fast lookups)
|
|
71
|
+
* - Database (PostgreSQL, MySQL, etc.)
|
|
72
|
+
*
|
|
73
|
+
* @template TExtended - Type-safe custom data that consumers can add to users
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* // Custom user data
|
|
78
|
+
* type MyUserData = {
|
|
79
|
+
* department: string;
|
|
80
|
+
* roles: string[];
|
|
81
|
+
* };
|
|
82
|
+
*
|
|
83
|
+
* // Implement custom store
|
|
84
|
+
* class RedisUserStore implements UserStore<MyUserData> {
|
|
85
|
+
* async get(sub: string): Promise<StoredUser<MyUserData> | null> {
|
|
86
|
+
* const data = await redis.get(`user:${sub}`);
|
|
87
|
+
* return data ? JSON.parse(data) : null;
|
|
88
|
+
* }
|
|
89
|
+
* // ... other methods
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export interface UserStore<TExtended = {}> {
|
|
94
|
+
/**
|
|
95
|
+
* Retrieve a user by their subject identifier (sub).
|
|
96
|
+
*
|
|
97
|
+
* @param sub - The user's unique identifier from the IdP
|
|
98
|
+
* @returns The user if found, null otherwise
|
|
99
|
+
*/
|
|
100
|
+
get(sub: string): Promise<StoredUser<TExtended> | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Retrieve a user by their email address.
|
|
103
|
+
*
|
|
104
|
+
* @param email - The user's email address
|
|
105
|
+
* @returns The user if found, null otherwise
|
|
106
|
+
*/
|
|
107
|
+
getByEmail(email: string): Promise<StoredUser<TExtended> | null>;
|
|
108
|
+
/**
|
|
109
|
+
* Retrieve a user by their username.
|
|
110
|
+
*
|
|
111
|
+
* @param userName - The user's username
|
|
112
|
+
* @returns The user if found, null otherwise
|
|
113
|
+
*/
|
|
114
|
+
getByUserName(userName: string): Promise<StoredUser<TExtended> | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Create or update a user in the store.
|
|
117
|
+
*
|
|
118
|
+
* If a user with the same `id` (sub) exists, it will be updated.
|
|
119
|
+
* Otherwise, a new user will be created.
|
|
120
|
+
*
|
|
121
|
+
* @param user - The user data to store
|
|
122
|
+
*/
|
|
123
|
+
upsert(user: StoredUser<TExtended>): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Delete a user by their subject identifier (sub).
|
|
126
|
+
*
|
|
127
|
+
* @param sub - The user's unique identifier to delete
|
|
128
|
+
*/
|
|
129
|
+
delete(sub: string): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* In-memory user store implementation using Maps.
|
|
133
|
+
*
|
|
134
|
+
* Suitable for:
|
|
135
|
+
* - Development and testing
|
|
136
|
+
* - Single-server deployments
|
|
137
|
+
* - Applications without high availability requirements
|
|
138
|
+
*
|
|
139
|
+
* NOT suitable for:
|
|
140
|
+
* - Multi-server deployments (users not shared)
|
|
141
|
+
* - High availability scenarios (users lost on restart)
|
|
142
|
+
* - Production applications with distributed architecture
|
|
143
|
+
*
|
|
144
|
+
* For production, implement UserStore with Redis or a database.
|
|
145
|
+
*
|
|
146
|
+
* @template TExtended - Type-safe custom data that consumers can add to users
|
|
147
|
+
*/
|
|
148
|
+
export declare class InMemoryUserStore<TExtended = {}> implements UserStore<TExtended> {
|
|
149
|
+
/** Primary storage: sub -> user */
|
|
150
|
+
private users;
|
|
151
|
+
/** Secondary index: email -> sub */
|
|
152
|
+
private emailIndex;
|
|
153
|
+
/** Secondary index: userName -> sub */
|
|
154
|
+
private userNameIndex;
|
|
155
|
+
get(sub: string): Promise<StoredUser<TExtended> | null>;
|
|
156
|
+
getByEmail(email: string): Promise<StoredUser<TExtended> | null>;
|
|
157
|
+
getByUserName(userName: string): Promise<StoredUser<TExtended> | null>;
|
|
158
|
+
upsert(user: StoredUser<TExtended>): Promise<void>;
|
|
159
|
+
delete(sub: string): Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=user-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-store.d.ts","sourceRoot":"","sources":["../src/user-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,CAAC,SAAS,GAAG,EAAE,IAAI,IAAI,GAAG;IAC9C;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,SAAS,EAAE,IAAI,CAAC;IAEhB;;OAEG;IACH,SAAS,EAAE,IAAI,CAAC;CACjB,GAAG,SAAS,CAAC;AAEd;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,SAAS,CAAC,SAAS,GAAG,EAAE;IACvC;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;IAExD;;;;;OAKG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;IAEjE;;;;;OAKG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;IAEvE;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,iBAAiB,CAAC,SAAS,GAAG,EAAE,CAAE,YAAW,SAAS,CAAC,SAAS,CAAC;IAC5E,mCAAmC;IACnC,OAAO,CAAC,KAAK,CAA4C;IAEzD,oCAAoC;IACpC,OAAO,CAAC,UAAU,CAA6B;IAE/C,uCAAuC;IACvC,OAAO,CAAC,aAAa,CAA6B;IAE5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAIvD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAMhE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAMtE,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBlD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAazC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { TokenValidationResult } from './types/workload-schema';
|
|
2
|
+
import type { ESConfig, WorkloadIdentity } from './workload';
|
|
3
|
+
/**
|
|
4
|
+
* Get the workload identity from an incoming request.
|
|
5
|
+
* Returns undefined if no valid workload token is present.
|
|
6
|
+
*
|
|
7
|
+
* @param request - Request with Authorization header
|
|
8
|
+
* @param config - Optional EnterpriseStandard configuration
|
|
9
|
+
* @returns WorkloadIdentity or undefined
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { getWorkload } from '@enterprisestandard/react';
|
|
14
|
+
*
|
|
15
|
+
* export async function handler(request: Request) {
|
|
16
|
+
* const workload = await getWorkload(request);
|
|
17
|
+
*
|
|
18
|
+
* if (!workload) {
|
|
19
|
+
* return new Response('Unauthorized', { status: 401 });
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* console.log('Request from workload:', workload.workload_id);
|
|
23
|
+
* // ... process authenticated request
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function getWorkload(request: Request, config?: ESConfig): Promise<WorkloadIdentity | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* Get an access token for the configured workload identity.
|
|
30
|
+
*
|
|
31
|
+
* @param scope - Optional OAuth2 scopes (space-delimited)
|
|
32
|
+
* @param config - Optional EnterpriseStandard configuration
|
|
33
|
+
* @returns Access token string
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { getWorkloadToken } from '@enterprisestandard/react/server';
|
|
38
|
+
*
|
|
39
|
+
* // Get token for API calls
|
|
40
|
+
* const token = await getWorkloadToken('api:read api:write');
|
|
41
|
+
*
|
|
42
|
+
* // Use in outbound requests
|
|
43
|
+
* const response = await fetch('https://api.example.com/data', {
|
|
44
|
+
* headers: { 'Authorization': `Bearer ${token}` },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function getWorkloadToken(scope?: string, config?: ESConfig): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Validate a workload token from an incoming request.
|
|
51
|
+
*
|
|
52
|
+
* @param request - Request with Authorization header
|
|
53
|
+
* @param config - Optional EnterpriseStandard configuration
|
|
54
|
+
* @returns Token validation result
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { validateWorkloadToken } from '@enterprisestandard/react/server';
|
|
59
|
+
*
|
|
60
|
+
* export async function handler(request: Request) {
|
|
61
|
+
* const result = await validateWorkloadToken(request);
|
|
62
|
+
*
|
|
63
|
+
* if (!result.valid) {
|
|
64
|
+
* return new Response(
|
|
65
|
+
* JSON.stringify({ error: result.error }),
|
|
66
|
+
* { status: 401 }
|
|
67
|
+
* );
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* const workloadId = result.claims?.iss;
|
|
71
|
+
* // ... process authenticated request
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function validateWorkloadToken(request: Request, config?: ESConfig): Promise<TokenValidationResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Revoke a workload access token.
|
|
78
|
+
*
|
|
79
|
+
* @param token - The access token to revoke
|
|
80
|
+
* @param config - Optional EnterpriseStandard configuration
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* import { revokeWorkloadToken } from '@enterprisestandard/react/server';
|
|
85
|
+
*
|
|
86
|
+
* // Revoke token when workload is decommissioned
|
|
87
|
+
* await revokeWorkloadToken(accessToken);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function revokeWorkloadToken(token: string, config?: ESConfig): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Framework-agnostic handler for workload authentication routes.
|
|
93
|
+
*
|
|
94
|
+
* The handler reads configuration (handler URLs, validation) directly from the
|
|
95
|
+
* EnterpriseStandard instance, so no config parameter is needed.
|
|
96
|
+
*
|
|
97
|
+
* @param request - Incoming request
|
|
98
|
+
* @param config - Optional ESConfig to specify which EnterpriseStandard instance to use
|
|
99
|
+
* @returns Response
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { workloadHandler } from '@enterprisestandard/react/server';
|
|
104
|
+
*
|
|
105
|
+
* // TanStack Start example
|
|
106
|
+
* export const Route = createFileRoute('/api/workload/$')({
|
|
107
|
+
* server: {
|
|
108
|
+
* handlers: ({ createHandlers }) =>
|
|
109
|
+
* createHandlers({
|
|
110
|
+
* GET: {
|
|
111
|
+
* handler: async ({ request }) => {
|
|
112
|
+
* return workloadHandler(request);
|
|
113
|
+
* },
|
|
114
|
+
* },
|
|
115
|
+
* POST: {
|
|
116
|
+
* handler: async ({ request }) => {
|
|
117
|
+
* return workloadHandler(request);
|
|
118
|
+
* },
|
|
119
|
+
* },
|
|
120
|
+
* }),
|
|
121
|
+
* },
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export declare function workloadHandler(request: Request, config?: ESConfig): Promise<Response>;
|
|
126
|
+
//# sourceMappingURL=workload-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-server.d.ts","sourceRoot":"","sources":["../src/workload-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAmB7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAM5G;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAIzF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAa/G;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAI5F"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token caching for workload identity authentication.
|
|
3
|
+
*
|
|
4
|
+
* Token stores are optional but recommended for performance - they enable:
|
|
5
|
+
* - Token caching to avoid repeated token acquisition
|
|
6
|
+
* - Automatic token refresh before expiration
|
|
7
|
+
* - Reduced load on authorization servers
|
|
8
|
+
*
|
|
9
|
+
* ## Token Caching Strategy
|
|
10
|
+
*
|
|
11
|
+
* Unlike session stores for user authentication, workload token stores cache
|
|
12
|
+
* short-lived access tokens (typically 5 minutes) for service-to-service calls.
|
|
13
|
+
*
|
|
14
|
+
* **Default Behavior:**
|
|
15
|
+
* - Tokens cached with 5-minute TTL
|
|
16
|
+
* - Auto-refresh 60 seconds before expiration
|
|
17
|
+
* - Expired tokens automatically removed
|
|
18
|
+
*
|
|
19
|
+
* ## Performance Characteristics
|
|
20
|
+
*
|
|
21
|
+
* | Backend | Lookup Time | Use Case |
|
|
22
|
+
* |--------------|-------------|----------------------------|
|
|
23
|
+
* | InMemory | <0.00005ms | Single-server deployments |
|
|
24
|
+
* | Redis | 1-2ms | Multi-server deployments |
|
|
25
|
+
* | Database | 5-20ms | Persistent token storage |
|
|
26
|
+
*
|
|
27
|
+
* ## Example Usage
|
|
28
|
+
*
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { workload, InMemoryWorkloadTokenStore } from '@enterprisestandard/react/server';
|
|
31
|
+
*
|
|
32
|
+
* // With token caching
|
|
33
|
+
* const workloadAuth = workload({
|
|
34
|
+
* // ... other config
|
|
35
|
+
* token_store: new InMemoryWorkloadTokenStore(),
|
|
36
|
+
* auto_refresh: true, // Refresh before expiry
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Without token caching (fetch new token each time)
|
|
40
|
+
* const workloadAuth = workload({
|
|
41
|
+
* // ... config without token_store
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Cached workload token data.
|
|
47
|
+
*
|
|
48
|
+
* @template TExtended - Type-safe custom data that consumers can add to cached tokens
|
|
49
|
+
*/
|
|
50
|
+
export type CachedWorkloadToken<TExtended = object> = {
|
|
51
|
+
/**
|
|
52
|
+
* Workload identifier (typically SPIFFE ID).
|
|
53
|
+
* Used as the primary key for token lookup.
|
|
54
|
+
*/
|
|
55
|
+
workload_id: string;
|
|
56
|
+
/**
|
|
57
|
+
* OAuth2 access token (Bearer token)
|
|
58
|
+
*/
|
|
59
|
+
access_token: string;
|
|
60
|
+
/**
|
|
61
|
+
* Token type (always "Bearer" for OAuth2)
|
|
62
|
+
*/
|
|
63
|
+
token_type: string;
|
|
64
|
+
/**
|
|
65
|
+
* OAuth2 scopes granted for this token
|
|
66
|
+
*/
|
|
67
|
+
scope?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Timestamp when the token expires.
|
|
70
|
+
* Used for automatic cleanup and refresh logic.
|
|
71
|
+
*/
|
|
72
|
+
expires_at: Date;
|
|
73
|
+
/**
|
|
74
|
+
* Timestamp when the token was created/cached.
|
|
75
|
+
*/
|
|
76
|
+
created_at: Date;
|
|
77
|
+
/**
|
|
78
|
+
* Optional refresh token (rarely used for workload identities)
|
|
79
|
+
*/
|
|
80
|
+
refresh_token?: string;
|
|
81
|
+
} & TExtended;
|
|
82
|
+
/**
|
|
83
|
+
* Abstract interface for workload token storage backends.
|
|
84
|
+
*
|
|
85
|
+
* Consumers can implement this interface to use different storage backends:
|
|
86
|
+
* - Redis (recommended for multi-server deployments)
|
|
87
|
+
* - Database (PostgreSQL, MySQL, etc. for persistence)
|
|
88
|
+
* - Distributed cache (Memcached, etc.)
|
|
89
|
+
* - Custom solutions
|
|
90
|
+
*
|
|
91
|
+
* @template TExtended - Type-safe custom data that consumers can add to cached tokens
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* // Custom token cache data
|
|
96
|
+
* type MyTokenData = {
|
|
97
|
+
* environment: string;
|
|
98
|
+
* region: string;
|
|
99
|
+
* };
|
|
100
|
+
*
|
|
101
|
+
* // Implement custom store with Redis
|
|
102
|
+
* class RedisWorkloadTokenStore implements WorkloadTokenStore<MyTokenData> {
|
|
103
|
+
* async set(token: CachedWorkloadToken<MyTokenData>): Promise<void> {
|
|
104
|
+
* const ttl = Math.floor((token.expires_at.getTime() - Date.now()) / 1000);
|
|
105
|
+
* await redis.setex(
|
|
106
|
+
* `workload:token:${token.workload_id}`,
|
|
107
|
+
* ttl,
|
|
108
|
+
* JSON.stringify(token)
|
|
109
|
+
* );
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* async get(workload_id: string): Promise<CachedWorkloadToken<MyTokenData> | null> {
|
|
113
|
+
* const data = await redis.get(`workload:token:${workload_id}`);
|
|
114
|
+
* if (!data) return null;
|
|
115
|
+
* const token = JSON.parse(data);
|
|
116
|
+
* // Convert date strings back to Date objects
|
|
117
|
+
* token.expires_at = new Date(token.expires_at);
|
|
118
|
+
* token.created_at = new Date(token.created_at);
|
|
119
|
+
* return token;
|
|
120
|
+
* }
|
|
121
|
+
*
|
|
122
|
+
* // ... other methods
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export interface WorkloadTokenStore<TExtended = object> {
|
|
127
|
+
/**
|
|
128
|
+
* Store or update a workload token in the cache.
|
|
129
|
+
*
|
|
130
|
+
* @param token - The token data to cache
|
|
131
|
+
*/
|
|
132
|
+
set(token: CachedWorkloadToken<TExtended>): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Retrieve a cached token by workload ID.
|
|
135
|
+
*
|
|
136
|
+
* @param workload_id - The workload identifier (e.g., SPIFFE ID)
|
|
137
|
+
* @returns The cached token if found and not expired, null otherwise
|
|
138
|
+
*/
|
|
139
|
+
get(workload_id: string): Promise<CachedWorkloadToken<TExtended> | null>;
|
|
140
|
+
/**
|
|
141
|
+
* Delete a cached token by workload ID.
|
|
142
|
+
*
|
|
143
|
+
* Used when explicitly revoking tokens or clearing cache.
|
|
144
|
+
*
|
|
145
|
+
* @param workload_id - The workload identifier to remove
|
|
146
|
+
*/
|
|
147
|
+
delete(workload_id: string): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Check if a valid (non-expired) token exists for a workload.
|
|
150
|
+
*
|
|
151
|
+
* @param workload_id - The workload identifier to check
|
|
152
|
+
* @returns true if a valid token exists, false otherwise
|
|
153
|
+
*/
|
|
154
|
+
isValid(workload_id: string): Promise<boolean>;
|
|
155
|
+
/**
|
|
156
|
+
* Remove all expired tokens from the cache.
|
|
157
|
+
*
|
|
158
|
+
* Should be called periodically to prevent memory leaks.
|
|
159
|
+
*/
|
|
160
|
+
cleanup(): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* In-memory workload token store implementation using Maps.
|
|
164
|
+
*
|
|
165
|
+
* Suitable for:
|
|
166
|
+
* - Development and testing
|
|
167
|
+
* - Single-server deployments
|
|
168
|
+
* - Applications without high availability requirements
|
|
169
|
+
*
|
|
170
|
+
* NOT suitable for:
|
|
171
|
+
* - Multi-server deployments (tokens not shared across instances)
|
|
172
|
+
* - High availability scenarios (tokens lost on restart)
|
|
173
|
+
* - Production applications with distributed architecture
|
|
174
|
+
*
|
|
175
|
+
* For production multi-server deployments, implement WorkloadTokenStore with Redis.
|
|
176
|
+
*
|
|
177
|
+
* @template TExtended - Type-safe custom data that consumers can add to cached tokens
|
|
178
|
+
*/
|
|
179
|
+
export declare class InMemoryWorkloadTokenStore<TExtended = object> implements WorkloadTokenStore<TExtended> {
|
|
180
|
+
private tokens;
|
|
181
|
+
set(token: CachedWorkloadToken<TExtended>): Promise<void>;
|
|
182
|
+
get(workload_id: string): Promise<CachedWorkloadToken<TExtended> | null>;
|
|
183
|
+
delete(workload_id: string): Promise<void>;
|
|
184
|
+
isValid(workload_id: string): Promise<boolean>;
|
|
185
|
+
cleanup(): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=workload-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-token-store.d.ts","sourceRoot":"","sources":["../src/workload-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,CAAC,SAAS,GAAG,MAAM,IAAI;IACpD;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,UAAU,EAAE,IAAI,CAAC;IAEjB;;OAEG;IACH,UAAU,EAAE,IAAI,CAAC;IAEjB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,SAAS,CAAC;AAEd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,kBAAkB,CAAC,SAAS,GAAG,MAAM;IACpD;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,mBAAmB,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1D;;;;;OAKG;IACH,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;IAEzE;;;;;;OAMG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/C;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,0BAA0B,CAAC,SAAS,GAAG,MAAM,CAAE,YAAW,kBAAkB,CAAC,SAAS,CAAC;IAClG,OAAO,CAAC,MAAM,CAAqD;IAE7D,GAAG,CAAC,KAAK,EAAE,mBAAmB,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAaxE,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK9C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ/B"}
|