@portalshq/capability-identity 0.2.1 → 0.4.0
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/CHANGELOG.md +18 -0
- package/dist/auth-gateway-client.d.ts +41 -0
- package/dist/auth-gateway-client.js +44 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @portalshq/capability-identity
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Expose the hosted sign-in session RPCs on `createAuthGatewayClient`:
|
|
6
|
+
`startAuthSession` returns the `loginUrl` and `sessionCode` to poll,
|
|
7
|
+
`getAuthSession` resolves `null` while sign-in is pending and the minted
|
|
8
|
+
`AuthSessionTokens` (user token, expiry, rotating `refreshToken`) once the
|
|
9
|
+
viewer completes it, `refreshAuthSession` rotates the refresh credential for
|
|
10
|
+
a new user token, and `revokeAuthSession` ends the session family. Refresh
|
|
11
|
+
and revoke carry the credential in `authorization` metadata per the RPC
|
|
12
|
+
contract. No auth-gateway changes are required — the service already
|
|
13
|
+
implements these methods.
|
|
14
|
+
|
|
15
|
+
## 0.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 31b4bb6: Promote identity from stub to usable: verified access-token resolution, PostgreSQL-backed cross-channel interaction history stamped with the transaction clock, and bounded identifiers and reads.
|
|
20
|
+
|
|
3
21
|
## 0.2.1
|
|
4
22
|
|
|
5
23
|
- Implement typed explicit auth-gateway sync with subject matching and clear unsupported-store errors.
|
|
@@ -2,10 +2,51 @@ export interface UserInfo {
|
|
|
2
2
|
userId: string;
|
|
3
3
|
displayName: string;
|
|
4
4
|
}
|
|
5
|
+
/** Returned by `startAuthSession`: send the viewer to `loginUrl`, then poll
|
|
6
|
+
* `getAuthSession` with the `sessionCode` until it yields tokens. */
|
|
7
|
+
export interface AuthSessionStart {
|
|
8
|
+
sessionCode: string;
|
|
9
|
+
loginUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/** A minted user credential. `refreshToken` is present only on session
|
|
12
|
+
* creation and refresh; it rotates on every refresh, so callers must persist
|
|
13
|
+
* the newest value or the next rotation fails as replay. */
|
|
14
|
+
export interface AuthSessionTokens {
|
|
15
|
+
userToken: string;
|
|
16
|
+
/** Epoch seconds when the user token expires, as issued by the gateway. */
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
userId: string;
|
|
19
|
+
userName: string;
|
|
20
|
+
refreshToken?: string;
|
|
21
|
+
}
|
|
5
22
|
export interface AuthGatewayClient {
|
|
6
23
|
verifyUser(token: string): Promise<UserInfo>;
|
|
7
24
|
getUserInfo(resourceId: string, userId: string): Promise<UserInfo>;
|
|
8
25
|
getUserId(resourceId: string, displayName: string): Promise<UserInfo>;
|
|
26
|
+
/**
|
|
27
|
+
* Begin a hosted sign-in. `clientState` must be a UUID generated by the
|
|
28
|
+
* caller; it binds the session to the requester and is required again when
|
|
29
|
+
* polling, so a different browser cannot consume the session.
|
|
30
|
+
*/
|
|
31
|
+
startAuthSession(clientState: string): Promise<AuthSessionStart>;
|
|
32
|
+
/**
|
|
33
|
+
* Poll a session started with `startAuthSession`. Resolves `null` while the
|
|
34
|
+
* viewer has not completed sign-in; resolves the minted tokens once they
|
|
35
|
+
* have. The gateway consumes the session on success — only the first
|
|
36
|
+
* successful poll receives tokens.
|
|
37
|
+
*/
|
|
38
|
+
getAuthSession(sessionCode: string, clientState: string): Promise<AuthSessionTokens | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Rotate a refresh credential for a new user token. The credential travels
|
|
41
|
+
* in the `authorization` metadata key because the RPC request is empty by
|
|
42
|
+
* contract. Resolves `null` when the gateway returns no token.
|
|
43
|
+
*/
|
|
44
|
+
refreshAuthSession(refreshToken: string): Promise<AuthSessionTokens | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Revoke the refresh credential's session family (sign-out). Best-effort:
|
|
47
|
+
* the gateway never reveals whether the token existed.
|
|
48
|
+
*/
|
|
49
|
+
revokeAuthSession(refreshToken: string): Promise<void>;
|
|
9
50
|
close(): void;
|
|
10
51
|
}
|
|
11
52
|
export interface AuthGatewayClientConfig {
|
|
@@ -16,12 +16,15 @@ export function createAuthGatewayClient(config) {
|
|
|
16
16
|
const Constructor = namespace.UrcAuthApi;
|
|
17
17
|
const client = new Constructor(config.address, config.useTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure());
|
|
18
18
|
let closed = false;
|
|
19
|
-
|
|
19
|
+
function unary(method, request, metadata) {
|
|
20
20
|
if (closed)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
client.makeUnaryRequest(`/epic_urc.UrcAuthApi/${method}`, (value) => methods[method].requestSerialize(value), (value) => methods[method].responseDeserialize(value), request, new grpc.Metadata(), { deadline: Date.now() + timeout }, (error, result) => error ? reject(error) : resolve(result ?? {}));
|
|
21
|
+
return Promise.reject(new Error("Auth-gateway client is closed."));
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
client.makeUnaryRequest(`/epic_urc.UrcAuthApi/${method}`, (value) => methods[method].requestSerialize(value), (value) => methods[method].responseDeserialize(value), request, metadata ?? new grpc.Metadata(), { deadline: Date.now() + timeout }, (error, result) => error ? reject(error) : resolve(result ?? {}));
|
|
24
24
|
});
|
|
25
|
+
}
|
|
26
|
+
async function call(method, request, expectedId) {
|
|
27
|
+
const response = await unary(method, request);
|
|
25
28
|
const candidates = Array.isArray(response.user_info) ? response.user_info : response.user_info ? [response.user_info] : [];
|
|
26
29
|
const user = expectedId ? candidates.find((candidate) => candidate.user_id === expectedId) : candidates[0];
|
|
27
30
|
if (!user || typeof user.user_id !== "string" || !user.user_id.trim() || typeof user.display_name !== "string") {
|
|
@@ -29,10 +32,47 @@ export function createAuthGatewayClient(config) {
|
|
|
29
32
|
}
|
|
30
33
|
return { userId: user.user_id, displayName: user.display_name };
|
|
31
34
|
}
|
|
35
|
+
/** The refresh credential rides in `authorization` metadata — the
|
|
36
|
+
* RefreshAuthSession and RevokeAuthSession requests are empty by contract. */
|
|
37
|
+
function refreshMetadata(refreshToken) {
|
|
38
|
+
const metadata = new grpc.Metadata();
|
|
39
|
+
metadata.add("authorization", `Bearer ${refreshToken}`);
|
|
40
|
+
return metadata;
|
|
41
|
+
}
|
|
42
|
+
function mapUserToken(response) {
|
|
43
|
+
const token = response.user_token;
|
|
44
|
+
if (token === null || token === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
if (typeof token.user_token !== "string" || !token.user_token.trim())
|
|
47
|
+
return null;
|
|
48
|
+
const expiresAt = Number(token.expires_at);
|
|
49
|
+
return {
|
|
50
|
+
userToken: token.user_token,
|
|
51
|
+
expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
|
|
52
|
+
userId: typeof token.user_id === "string" ? token.user_id : "",
|
|
53
|
+
userName: typeof token.user_name === "string" ? token.user_name : "",
|
|
54
|
+
...(typeof token.refresh_token === "string" && token.refresh_token ? { refreshToken: token.refresh_token } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
32
57
|
return {
|
|
33
58
|
verifyUser: (token) => call("VerifyUser", { target_user: { user_token: token }, requirements: [] }),
|
|
34
59
|
getUserInfo: (resourceId, userId) => call("GetUserInfo", { resource_id: resourceId, user_id: [userId] }, userId),
|
|
35
60
|
getUserId: (resourceId, displayName) => call("GetUserId", { resource_id: resourceId, user_display_name: displayName }),
|
|
61
|
+
async startAuthSession(clientState) {
|
|
62
|
+
const response = await unary("StartAuthSession", { client_state: clientState });
|
|
63
|
+
if (typeof response.session_code !== "string" || !response.session_code.trim()
|
|
64
|
+
|| typeof response.login_url !== "string" || !response.login_url.trim()) {
|
|
65
|
+
throw new Error("Auth-gateway StartAuthSession returned no usable session");
|
|
66
|
+
}
|
|
67
|
+
return { sessionCode: response.session_code, loginUrl: response.login_url };
|
|
68
|
+
},
|
|
69
|
+
getAuthSession: (sessionCode, clientState) => unary("GetAuthSession", { session_code: sessionCode, client_state: clientState })
|
|
70
|
+
.then(mapUserToken),
|
|
71
|
+
refreshAuthSession: (refreshToken) => unary("RefreshAuthSession", {}, refreshMetadata(refreshToken))
|
|
72
|
+
.then(mapUserToken),
|
|
73
|
+
revokeAuthSession: async (refreshToken) => {
|
|
74
|
+
await unary("RevokeAuthSession", {}, refreshMetadata(refreshToken));
|
|
75
|
+
},
|
|
36
76
|
close: () => { if (!closed) {
|
|
37
77
|
closed = true;
|
|
38
78
|
client.close();
|