@baseworks/iam 1.2.0 → 1.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.
@@ -17,6 +17,13 @@ var UserEvents = defineAggregate("user", {
17
17
  name: z.string().nullable(),
18
18
  picture: z.string().nullable(),
19
19
  at: z.number().int()
20
+ }),
21
+ // The person chose what they are called here (iam-service 0.5). Kept apart
22
+ // from `name`, which the identity provider sends on every sign-in and would
23
+ // otherwise overwrite it. `null` means "back to what the provider says".
24
+ UserRenamed: z.object({
25
+ displayName: z.string().min(1).nullable(),
26
+ at: z.number().int()
20
27
  })
21
28
  });
22
29
  var TOKEN_TYPES = ["user", "service", "cli", "agent"];
package/dist/cli.js CHANGED
@@ -17,21 +17,10 @@ function buildIamCommand(deps) {
17
17
  ["id", user.id]
18
18
  ]);
19
19
  });
20
- const users = iam.command("users").description("user directory");
21
- users.command("find <email>").description("resolve a user id by exact email (for invites)").action(async (email) => {
22
- const { user } = await http.get(
23
- `/users/lookup?email=${encodeURIComponent(email)}`
24
- ).catch(die);
25
- kv([
26
- ["id", user.id],
27
- ["email", user.email],
28
- ["name", user.name ?? "\u2014"]
29
- ]);
30
- });
31
20
  const token = iam.command("token").description("api tokens (service / cli / agent credentials)");
32
21
  outputOpt(token.command("list")).description("active tokens you can see").action(async (opts) => {
33
22
  const { tokens } = await http.get(
34
- "/v1/tokens"
23
+ "/tokens"
35
24
  ).catch(die);
36
25
  printOutput(
37
26
  tokens,
@@ -45,7 +34,7 @@ function buildIamCommand(deps) {
45
34
  );
46
35
  });
47
36
  token.command("issue <name>").description("issue a token; the secret is printed once").option("--type <type>", "user | service | cli | agent", "service").option("--org <id>", "bind to an organization (opaque context)").option("--scope <scope...>", "grant scopes").action(async (name, opts) => {
48
- const res = await http.post("/v1/tokens", {
37
+ const res = await http.post("/tokens", {
49
38
  name,
50
39
  type: opts.type,
51
40
  org_id: opts.org ?? null,
@@ -58,7 +47,7 @@ function buildIamCommand(deps) {
58
47
  ]);
59
48
  });
60
49
  token.command("revoke <id>").description("revoke a token by id").action(async (id) => {
61
- await http.del(`/v1/tokens/${encodeURIComponent(id)}`).catch(die);
50
+ await http.del(`/tokens/${encodeURIComponent(id)}`).catch(die);
62
51
  success(`token revoked: ${id}`);
63
52
  });
64
53
  return iam;
package/dist/client.d.ts CHANGED
@@ -10,6 +10,8 @@ interface UserRow {
10
10
  email: string;
11
11
  name: string | null;
12
12
  picture: string | null;
13
+ /** What the person chose to be called here; null or absent: the provider's `name`. */
14
+ display_name?: string | null;
13
15
  created_at: number;
14
16
  updated_at: number;
15
17
  }
@@ -38,12 +40,27 @@ interface User {
38
40
  created_at: number;
39
41
  updated_at: number;
40
42
  }
41
- /** Public-safe identity projection (returned by /me and /users/lookup). */
43
+ /**
44
+ * Public-safe identity projection (returned by /users/lookup and
45
+ * /users/resolve). `name` is what the person chose to be called, else what
46
+ * their identity provider says.
47
+ */
42
48
  interface PublicUser {
43
49
  id: string;
44
50
  email: string;
45
51
  name: string | null;
46
52
  }
53
+ /** A person's own account, as `/me` gives it to them and to nobody else. */
54
+ interface Me extends PublicUser {
55
+ /** The name the identity provider sends; what `name` falls back to. */
56
+ provider_name: string | null;
57
+ /** True when `name` is one the person set here. */
58
+ name_is_own: boolean;
59
+ picture: string | null;
60
+ /** The identity provider this account signs in with. */
61
+ issuer: string;
62
+ created_at: number;
63
+ }
47
64
  /** An OIDC identity presented to POST /sync (before a user id exists). */
48
65
  interface OidcIdentity {
49
66
  subject: string;
@@ -137,8 +154,15 @@ declare class IamClient {
137
154
  api<T>(method: string, path: string, body?: unknown, extra?: Record<string, string>): Promise<T>;
138
155
  /** Generic ApiClient (get/post/del) — used by the CLI plugin. */
139
156
  get http(): HttpClient;
140
- /** The signed-in user's own identity (no orgs — those live in @baseworks/org). */
141
- me(): Promise<PublicUser>;
157
+ /** The signed-in user's own account (no orgs — those live in @baseworks/org). */
158
+ me(): Promise<Me>;
159
+ /**
160
+ * Set what the signed-in person is called here. `null` goes back to the name
161
+ * their identity provider sends. Email and picture are the provider's.
162
+ */
163
+ updateMe(input: {
164
+ name: string | null;
165
+ }): Promise<Me>;
142
166
  /** Upsert an OIDC identity → the platform user. Called by auth-service (no auth). */
143
167
  sync(identity: OidcIdentity): Promise<{
144
168
  id: string;
@@ -175,4 +199,4 @@ declare class IamClient {
175
199
  /** Factory used by the SDK capability registry. */
176
200
  declare function createIamClient(opts: IamClientOptions): IamClient;
177
201
 
178
- export { type ApiToken, type ApiTokenRow, type AuthContext, type HttpClient, IamClient, type IamClientOptions, IamError, type IssuedToken, type OidcIdentity, type PublicUser, type TokenPrincipal, TokenType, type User, type UserRow, createIamClient };
202
+ export { type ApiToken, type ApiTokenRow, type AuthContext, type HttpClient, IamClient, type IamClientOptions, IamError, type IssuedToken, type Me, type OidcIdentity, type PublicUser, type TokenPrincipal, TokenType, type User, type UserRow, createIamClient };
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TOKEN_TYPES
3
- } from "./chunk-26XPWBFV.js";
3
+ } from "./chunk-NAVQMKZ3.js";
4
4
 
5
5
  // src/client.ts
6
6
  var IamError = class extends Error {
@@ -62,11 +62,19 @@ var IamClient = class _IamClient {
62
62
  };
63
63
  }
64
64
  // ── identity ────────────────────────────────────────────────────────────────
65
- /** The signed-in user's own identity (no orgs — those live in @baseworks/org). */
65
+ /** The signed-in user's own account (no orgs — those live in @baseworks/org). */
66
66
  async me() {
67
67
  const { user } = await this.api("GET", "/me");
68
68
  return user;
69
69
  }
70
+ /**
71
+ * Set what the signed-in person is called here. `null` goes back to the name
72
+ * their identity provider sends. Email and picture are the provider's.
73
+ */
74
+ async updateMe(input) {
75
+ const { user } = await this.api("PATCH", "/me", input);
76
+ return user;
77
+ }
70
78
  /** Upsert an OIDC identity → the platform user. Called by auth-service (no auth). */
71
79
  async sync(identity) {
72
80
  const { user } = await this.api("POST", "/sync", identity);
package/dist/events.d.ts CHANGED
@@ -55,6 +55,16 @@ declare const UserEvents: _baseworks_foldbase.EventCatalog<{
55
55
  name: string | null;
56
56
  picture: string | null;
57
57
  }>;
58
+ UserRenamed: z.ZodObject<{
59
+ displayName: z.ZodNullable<z.ZodString>;
60
+ at: z.ZodNumber;
61
+ }, "strip", z.ZodTypeAny, {
62
+ at: number;
63
+ displayName: string | null;
64
+ }, {
65
+ at: number;
66
+ displayName: string | null;
67
+ }>;
58
68
  }> & {
59
69
  newId(): string;
60
70
  };
package/dist/events.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TOKEN_TYPES,
4
4
  TokenType,
5
5
  UserEvents
6
- } from "./chunk-26XPWBFV.js";
6
+ } from "./chunk-NAVQMKZ3.js";
7
7
  export {
8
8
  ApiTokenEvents,
9
9
  TOKEN_TYPES,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ApiTokenEvents,
3
3
  UserEvents
4
- } from "./chunk-26XPWBFV.js";
4
+ } from "./chunk-NAVQMKZ3.js";
5
5
 
6
6
  // src/projections.ts
7
7
  import { defineProjection } from "@baseworks/foldbase";
@@ -18,6 +18,9 @@ var readUsers = defineProjection("users", UserEvents, (on) => ({
18
18
  email: e.email,
19
19
  name: e.name,
20
20
  picture: e.picture
21
+ })),
22
+ UserRenamed: on.UserRenamed.upsert((e) => ({
23
+ display_name: e.displayName
21
24
  }))
22
25
  }));
23
26
  var readApiTokens = defineProjection("api_tokens", ApiTokenEvents, (on) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baseworks/iam",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "IAM capability: identity (users) + credentials (api tokens). Typed HTTP client, foldbase event catalog, projections, and CLI plugin for iam-service. Contract-first, versioned with the service. Roles/PDP live in @baseworks/org.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -20,7 +20,7 @@
20
20
  "@baseworks/cli": "^0.2.0",
21
21
  "commander": "^15.0.0",
22
22
  "zod": "^3.25.76",
23
- "@baseworks/foldbase": "0.1.1"
23
+ "@baseworks/foldbase": "0.2.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.20.0",
@@ -27,15 +27,12 @@ describe('flect iam whoami', () => {
27
27
  })
28
28
  })
29
29
 
30
- // ── users find ────────────────────────────────────────────────────────────────
30
+ // ── no users find ─────────────────────────────────────────────────────────────
31
31
 
32
- describe('flect iam users find', () => {
33
- it('resolves a user id by email', async () => {
34
- mocks.mockGet('/users/lookup', { user: USER })
32
+ describe('flect iam users', () => {
33
+ it('is gone: looking a person up by email is for services, not for a CLI', async () => {
35
34
  const r = await run(['iam', 'users', 'find', 'ali@acme.test'])
36
- expect(r.exitCode).toBe(0)
37
- expect(r.stdout).toContain(USER.id)
38
- expect(r.stdout).toContain('ali@acme.test')
35
+ expect(r.stdout + r.stderr).toMatch(/unknown command/)
39
36
  })
40
37
  })
41
38
 
@@ -43,7 +40,7 @@ describe('flect iam users find', () => {
43
40
 
44
41
  describe('flect iam token', () => {
45
42
  it('list renders NAME / TYPE / PREFIX columns', async () => {
46
- mocks.mockGet('/v1/tokens', {
43
+ mocks.mockGet('/tokens', {
47
44
  tokens: [{ id: 't1', name: 'ci', type: 'service', key_prefix: 'bwt_abc12345', expires_at: null }],
48
45
  })
49
46
  const r = await run(['iam', 'token', 'list'])
@@ -54,14 +51,14 @@ describe('flect iam token', () => {
54
51
  })
55
52
 
56
53
  it('issue prints the one-time secret', async () => {
57
- mocks.mockPost('/v1/tokens', { token: { id: 't1', key_prefix: 'bwt_secret1234' }, secret: 'bwt_secret1234_rest' })
54
+ mocks.mockPost('/tokens', { token: { id: 't1', key_prefix: 'bwt_secret1234' }, secret: 'bwt_secret1234_rest' })
58
55
  const r = await run(['iam', 'token', 'issue', 'ci', '--type', 'service'])
59
56
  expect(r.exitCode).toBe(0)
60
57
  expect(r.stdout).toContain('bwt_secret1234_rest')
61
58
  })
62
59
 
63
60
  it('revoke calls DELETE and reports success', async () => {
64
- mocks.mockDelete('/v1/tokens/t1')
61
+ mocks.mockDelete('/tokens/t1')
65
62
  const r = await run(['iam', 'token', 'revoke', 't1'])
66
63
  expect(r.exitCode).toBe(0)
67
64
  expect(r.stdout).toContain('t1')
package/src/cli.ts CHANGED
@@ -41,24 +41,11 @@ export function buildIamCommand(deps: IamCliDeps): Command {
41
41
  ])
42
42
  })
43
43
 
44
- // ── users ─────────────────────────────────────────────────────────────────--
45
- const users = iam.command('users').description('user directory')
46
-
47
- users
48
- .command('find <email>')
49
- .description('resolve a user id by exact email (for invites)')
50
- .action(async (email: string) => {
51
- const { user } = await http
52
- .get<{ user: { id: string; email: string; name: string | null } }>(
53
- `/users/lookup?email=${encodeURIComponent(email)}`,
54
- )
55
- .catch(die)
56
- kv([
57
- ['id', user.id],
58
- ['email', user.email],
59
- ['name', user.name ?? '—'],
60
- ])
61
- })
44
+ // There is no `users find <email>`. It asked iam "does this address have an
45
+ // account here?" with a person's token, which is an account-existence oracle;
46
+ // iam answers that to services only now. Invite by email instead
47
+ // (`org invite <email>`): the invitation reaches the person without anyone
48
+ // learning their user id first.
62
49
 
63
50
  // ── token (credentials) ───────────────────────────────────────────────────--
64
51
  const token = iam.command('token').description('api tokens (service / cli / agent credentials)')
@@ -68,7 +55,7 @@ export function buildIamCommand(deps: IamCliDeps): Command {
68
55
  .action(async (opts: { output?: string }) => {
69
56
  const { tokens } = await http
70
57
  .get<{ tokens: Array<{ id: string; name: string; type: string; key_prefix: string; expires_at: number | null }> }>(
71
- '/v1/tokens',
58
+ '/tokens',
72
59
  )
73
60
  .catch(die)
74
61
  printOutput(tokens as unknown as Record<string, unknown>[], opts.output, () =>
@@ -89,7 +76,7 @@ export function buildIamCommand(deps: IamCliDeps): Command {
89
76
  .option('--scope <scope...>', 'grant scopes')
90
77
  .action(async (name: string, opts: { type?: string; org?: string; scope?: string[] }) => {
91
78
  const res = await http
92
- .post<{ token: { id: string; key_prefix: string }; secret: string }>('/v1/tokens', {
79
+ .post<{ token: { id: string; key_prefix: string }; secret: string }>('/tokens', {
93
80
  name,
94
81
  type: opts.type,
95
82
  org_id: opts.org ?? null,
@@ -107,7 +94,7 @@ export function buildIamCommand(deps: IamCliDeps): Command {
107
94
  .command('revoke <id>')
108
95
  .description('revoke a token by id')
109
96
  .action(async (id: string) => {
110
- await http.del(`/v1/tokens/${encodeURIComponent(id)}`).catch(die)
97
+ await http.del(`/tokens/${encodeURIComponent(id)}`).catch(die)
111
98
  success(`token revoked: ${id}`)
112
99
  })
113
100
 
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PublicUser, OidcIdentity, ApiToken, IssuedToken, TokenPrincipal, TokenType } from './types.js'
1
+ import type { PublicUser, Me, OidcIdentity, ApiToken, IssuedToken, TokenPrincipal, TokenType } from './types.js'
2
2
 
3
3
  // Value re-export (a runtime const, not just a type) so consumers can build
4
4
  // validators from it, e.g. `z.enum(TOKEN_TYPES)`.
@@ -7,6 +7,7 @@ export { TOKEN_TYPES } from './types.js'
7
7
  export type {
8
8
  User,
9
9
  PublicUser,
10
+ Me,
10
11
  OidcIdentity,
11
12
  ApiToken,
12
13
  IssuedToken,
@@ -112,9 +113,18 @@ export class IamClient {
112
113
 
113
114
  // ── identity ────────────────────────────────────────────────────────────────
114
115
 
115
- /** The signed-in user's own identity (no orgs — those live in @baseworks/org). */
116
- async me(): Promise<PublicUser> {
117
- const { user } = await this.api<{ user: PublicUser }>('GET', '/me')
116
+ /** The signed-in user's own account (no orgs — those live in @baseworks/org). */
117
+ async me(): Promise<Me> {
118
+ const { user } = await this.api<{ user: Me }>('GET', '/me')
119
+ return user
120
+ }
121
+
122
+ /**
123
+ * Set what the signed-in person is called here. `null` goes back to the name
124
+ * their identity provider sends. Email and picture are the provider's.
125
+ */
126
+ async updateMe(input: { name: string | null }): Promise<Me> {
127
+ const { user } = await this.api<{ user: Me }>('PATCH', '/me', input)
118
128
  return user
119
129
  }
120
130
 
package/src/events.ts CHANGED
@@ -36,6 +36,13 @@ export const UserEvents = defineAggregate('user', {
36
36
  picture: z.string().nullable(),
37
37
  at: z.number().int(),
38
38
  }),
39
+ // The person chose what they are called here (iam-service 0.5). Kept apart
40
+ // from `name`, which the identity provider sends on every sign-in and would
41
+ // otherwise overwrite it. `null` means "back to what the provider says".
42
+ UserRenamed: z.object({
43
+ displayName: z.string().min(1).nullable(),
44
+ at: z.number().int(),
45
+ }),
39
46
  })
40
47
 
41
48
  // ── api_token ───────────────────────────────────────────────────────────────
@@ -25,6 +25,9 @@ export const readUsers = defineProjection('users', UserEvents, (on) => ({
25
25
  name: e.name,
26
26
  picture: e.picture,
27
27
  })),
28
+ UserRenamed: on.UserRenamed.upsert((e) => ({
29
+ display_name: e.displayName,
30
+ })),
28
31
  }))
29
32
 
30
33
  export const readApiTokens = defineProjection('api_tokens', ApiTokenEvents, (on) => ({
package/src/types.ts CHANGED
@@ -14,6 +14,8 @@ export interface UserRow {
14
14
  email: string
15
15
  name: string | null
16
16
  picture: string | null
17
+ /** What the person chose to be called here; null or absent: the provider's `name`. */
18
+ display_name?: string | null
17
19
  created_at: number
18
20
  updated_at: number
19
21
  }
@@ -51,13 +53,29 @@ export interface User {
51
53
  updated_at: number
52
54
  }
53
55
 
54
- /** Public-safe identity projection (returned by /me and /users/lookup). */
56
+ /**
57
+ * Public-safe identity projection (returned by /users/lookup and
58
+ * /users/resolve). `name` is what the person chose to be called, else what
59
+ * their identity provider says.
60
+ */
55
61
  export interface PublicUser {
56
62
  id: string
57
63
  email: string
58
64
  name: string | null
59
65
  }
60
66
 
67
+ /** A person's own account, as `/me` gives it to them and to nobody else. */
68
+ export interface Me extends PublicUser {
69
+ /** The name the identity provider sends; what `name` falls back to. */
70
+ provider_name: string | null
71
+ /** True when `name` is one the person set here. */
72
+ name_is_own: boolean
73
+ picture: string | null
74
+ /** The identity provider this account signs in with. */
75
+ issuer: string
76
+ created_at: number
77
+ }
78
+
61
79
  /** An OIDC identity presented to POST /sync (before a user id exists). */
62
80
  export interface OidcIdentity {
63
81
  subject: string