@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 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
- async function call(method, request, expectedId) {
19
+ function unary(method, request, metadata) {
20
20
  if (closed)
21
- throw new Error("Auth-gateway client is closed.");
22
- const response = await new Promise((resolve, reject) => {
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portalshq/capability-identity",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/portalshq/portals-cloud.git",