@baseworks/iam 0.2.0 → 1.2.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/dist/cli.js +2 -2
- package/dist/client.d.ts +39 -16
- package/dist/client.js +21 -5
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +2 -2
- package/src/cli.ts +4 -4
- package/src/client.ts +25 -8
- package/src/types.ts +27 -13
package/dist/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ function buildIamCommand(deps) {
|
|
|
39
39
|
() => table(tokens, [
|
|
40
40
|
{ key: "name", label: "NAME" },
|
|
41
41
|
{ key: "type", label: "TYPE" },
|
|
42
|
-
{ key: "
|
|
42
|
+
{ key: "key_prefix", label: "PREFIX" },
|
|
43
43
|
{ key: "id", label: "ID" }
|
|
44
44
|
])
|
|
45
45
|
);
|
|
@@ -48,7 +48,7 @@ function buildIamCommand(deps) {
|
|
|
48
48
|
const res = await http.post("/v1/tokens", {
|
|
49
49
|
name,
|
|
50
50
|
type: opts.type,
|
|
51
|
-
|
|
51
|
+
org_id: opts.org ?? null,
|
|
52
52
|
scopes: opts.scope ?? []
|
|
53
53
|
}).catch(die);
|
|
54
54
|
success(`token issued: ${res.token.id}`);
|
package/dist/client.d.ts
CHANGED
|
@@ -35,8 +35,8 @@ interface User {
|
|
|
35
35
|
email: string;
|
|
36
36
|
name: string | null;
|
|
37
37
|
picture: string | null;
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
created_at: number;
|
|
39
|
+
updated_at: number;
|
|
40
40
|
}
|
|
41
41
|
/** Public-safe identity projection (returned by /me and /users/lookup). */
|
|
42
42
|
interface PublicUser {
|
|
@@ -56,16 +56,16 @@ interface OidcIdentity {
|
|
|
56
56
|
interface ApiToken {
|
|
57
57
|
id: string;
|
|
58
58
|
type: TokenType;
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
org_id: string | null;
|
|
60
|
+
user_id: string | null;
|
|
61
61
|
name: string;
|
|
62
|
-
|
|
62
|
+
key_prefix: string;
|
|
63
63
|
scopes: string[];
|
|
64
64
|
metadata: Record<string, unknown>;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
expires_at: number | null;
|
|
66
|
+
last_used_at: number | null;
|
|
67
|
+
created_at: number;
|
|
68
|
+
updated_at: number;
|
|
69
69
|
}
|
|
70
70
|
/** The one-time issue response — `token` is the secret, shown only here. */
|
|
71
71
|
interface IssuedToken {
|
|
@@ -75,11 +75,21 @@ interface IssuedToken {
|
|
|
75
75
|
}
|
|
76
76
|
/** The identity a verified token resolves to. */
|
|
77
77
|
interface TokenPrincipal {
|
|
78
|
-
|
|
78
|
+
token_id: string;
|
|
79
79
|
type: TokenType;
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
org_id: string | null;
|
|
81
|
+
user_id: string | null;
|
|
82
82
|
scopes: string[];
|
|
83
|
+
/** The name it was issued under, e.g. `notes-api@staging`. */
|
|
84
|
+
name: string;
|
|
85
|
+
/**
|
|
86
|
+
* What the issuer bound the token to, returned as it was stored. A service
|
|
87
|
+
* that issues tokens for its own workloads keeps the binding here (for
|
|
88
|
+
* example the scope and app a runtime token belongs to) and checks it on
|
|
89
|
+
* every verify, because iam keeps it opaque and never interprets it.
|
|
90
|
+
*/
|
|
91
|
+
metadata: Record<string, unknown>;
|
|
92
|
+
expires_at: number | null;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/** Forwarded end-user identity (X-Auth-*) for a trusted service caller. */
|
|
@@ -135,16 +145,29 @@ declare class IamClient {
|
|
|
135
145
|
}>;
|
|
136
146
|
/** Resolve a user id by exact email (invite flows). */
|
|
137
147
|
lookupByEmail(email: string): Promise<PublicUser>;
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a list of user ids to public profiles, in one round trip.
|
|
150
|
+
*
|
|
151
|
+
* Ids that resolve to nobody are absent from the answer rather than an error:
|
|
152
|
+
* one deleted user must not break a page that is otherwise fine.
|
|
153
|
+
*/
|
|
154
|
+
resolveUsers(ids: string[]): Promise<PublicUser[]>;
|
|
138
155
|
issueToken(input: {
|
|
139
156
|
name: string;
|
|
140
157
|
type?: TokenType;
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
org_id?: string | null;
|
|
159
|
+
user_id?: string | null;
|
|
143
160
|
scopes?: string[];
|
|
144
|
-
|
|
161
|
+
expires_at?: number | null;
|
|
145
162
|
metadata?: Record<string, unknown>;
|
|
146
163
|
}): Promise<IssuedToken>;
|
|
147
|
-
|
|
164
|
+
/**
|
|
165
|
+
* The caller's own tokens, or with `org_id` an organisation's — the latter
|
|
166
|
+
* only for a service named in the realm's IAM_ORG_TOKEN_ISSUERS.
|
|
167
|
+
*/
|
|
168
|
+
listTokens(opts?: {
|
|
169
|
+
org_id?: string;
|
|
170
|
+
}): Promise<ApiToken[]>;
|
|
148
171
|
revokeToken(id: string): Promise<void>;
|
|
149
172
|
/** Verify a raw token secret → its principal, or throw IamError(401). */
|
|
150
173
|
verifyToken(secret: string): Promise<TokenPrincipal>;
|
package/dist/client.js
CHANGED
|
@@ -77,20 +77,36 @@ var IamClient = class _IamClient {
|
|
|
77
77
|
const { user } = await this.api("GET", `/users/lookup?email=${encodeURIComponent(email)}`);
|
|
78
78
|
return user;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a list of user ids to public profiles, in one round trip.
|
|
82
|
+
*
|
|
83
|
+
* Ids that resolve to nobody are absent from the answer rather than an error:
|
|
84
|
+
* one deleted user must not break a page that is otherwise fine.
|
|
85
|
+
*/
|
|
86
|
+
async resolveUsers(ids) {
|
|
87
|
+
if (ids.length === 0) return [];
|
|
88
|
+
const { users } = await this.api("POST", "/users/resolve", { ids });
|
|
89
|
+
return users;
|
|
90
|
+
}
|
|
80
91
|
// ── credentials (api tokens) ─────────────────────────────────────────────────
|
|
81
92
|
async issueToken(input) {
|
|
82
|
-
return this.api("POST", "/
|
|
93
|
+
return this.api("POST", "/tokens", input);
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
/**
|
|
96
|
+
* The caller's own tokens, or with `org_id` an organisation's — the latter
|
|
97
|
+
* only for a service named in the realm's IAM_ORG_TOKEN_ISSUERS.
|
|
98
|
+
*/
|
|
99
|
+
async listTokens(opts = {}) {
|
|
100
|
+
const q = opts.org_id ? `?${new URLSearchParams({ org_id: opts.org_id })}` : "";
|
|
101
|
+
const { tokens } = await this.api("GET", `/tokens${q}`);
|
|
86
102
|
return tokens;
|
|
87
103
|
}
|
|
88
104
|
async revokeToken(id) {
|
|
89
|
-
await this.api("DELETE", `/
|
|
105
|
+
await this.api("DELETE", `/tokens/${encodeURIComponent(id)}`);
|
|
90
106
|
}
|
|
91
107
|
/** Verify a raw token secret → its principal, or throw IamError(401). */
|
|
92
108
|
async verifyToken(secret) {
|
|
93
|
-
const { principal } = await this.api("POST", "/
|
|
109
|
+
const { principal } = await this.api("POST", "/tokens/verify", { token: secret });
|
|
94
110
|
return principal;
|
|
95
111
|
}
|
|
96
112
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baseworks/iam",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.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": {
|
|
@@ -44,7 +44,7 @@ describe('flect iam users find', () => {
|
|
|
44
44
|
describe('flect iam token', () => {
|
|
45
45
|
it('list renders NAME / TYPE / PREFIX columns', async () => {
|
|
46
46
|
mocks.mockGet('/v1/tokens', {
|
|
47
|
-
tokens: [{ id: 't1', name: 'ci', type: 'service',
|
|
47
|
+
tokens: [{ id: 't1', name: 'ci', type: 'service', key_prefix: 'bwt_abc12345', expires_at: null }],
|
|
48
48
|
})
|
|
49
49
|
const r = await run(['iam', 'token', 'list'])
|
|
50
50
|
expect(r.exitCode).toBe(0)
|
|
@@ -54,7 +54,7 @@ describe('flect iam token', () => {
|
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('issue prints the one-time secret', async () => {
|
|
57
|
-
mocks.mockPost('/v1/tokens', { token: { id: 't1',
|
|
57
|
+
mocks.mockPost('/v1/tokens', { token: { id: 't1', key_prefix: 'bwt_secret1234' }, secret: 'bwt_secret1234_rest' })
|
|
58
58
|
const r = await run(['iam', 'token', 'issue', 'ci', '--type', 'service'])
|
|
59
59
|
expect(r.exitCode).toBe(0)
|
|
60
60
|
expect(r.stdout).toContain('bwt_secret1234_rest')
|
package/src/cli.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function buildIamCommand(deps: IamCliDeps): Command {
|
|
|
67
67
|
.description('active tokens you can see')
|
|
68
68
|
.action(async (opts: { output?: string }) => {
|
|
69
69
|
const { tokens } = await http
|
|
70
|
-
.get<{ tokens: Array<{ id: string; name: string; type: string;
|
|
70
|
+
.get<{ tokens: Array<{ id: string; name: string; type: string; key_prefix: string; expires_at: number | null }> }>(
|
|
71
71
|
'/v1/tokens',
|
|
72
72
|
)
|
|
73
73
|
.catch(die)
|
|
@@ -75,7 +75,7 @@ export function buildIamCommand(deps: IamCliDeps): Command {
|
|
|
75
75
|
table(tokens as unknown as Record<string, unknown>[], [
|
|
76
76
|
{ key: 'name', label: 'NAME' },
|
|
77
77
|
{ key: 'type', label: 'TYPE' },
|
|
78
|
-
{ key: '
|
|
78
|
+
{ key: 'key_prefix', label: 'PREFIX' },
|
|
79
79
|
{ key: 'id', label: 'ID' },
|
|
80
80
|
]),
|
|
81
81
|
)
|
|
@@ -89,10 +89,10 @@ export function buildIamCommand(deps: IamCliDeps): Command {
|
|
|
89
89
|
.option('--scope <scope...>', 'grant scopes')
|
|
90
90
|
.action(async (name: string, opts: { type?: string; org?: string; scope?: string[] }) => {
|
|
91
91
|
const res = await http
|
|
92
|
-
.post<{ token: { id: string;
|
|
92
|
+
.post<{ token: { id: string; key_prefix: string }; secret: string }>('/v1/tokens', {
|
|
93
93
|
name,
|
|
94
94
|
type: opts.type,
|
|
95
|
-
|
|
95
|
+
org_id: opts.org ?? null,
|
|
96
96
|
scopes: opts.scope ?? [],
|
|
97
97
|
})
|
|
98
98
|
.catch(die)
|
package/src/client.ts
CHANGED
|
@@ -130,32 +130,49 @@ export class IamClient {
|
|
|
130
130
|
return user
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a list of user ids to public profiles, in one round trip.
|
|
135
|
+
*
|
|
136
|
+
* Ids that resolve to nobody are absent from the answer rather than an error:
|
|
137
|
+
* one deleted user must not break a page that is otherwise fine.
|
|
138
|
+
*/
|
|
139
|
+
async resolveUsers(ids: string[]): Promise<PublicUser[]> {
|
|
140
|
+
if (ids.length === 0) return []
|
|
141
|
+
const { users } = await this.api<{ users: PublicUser[] }>('POST', '/users/resolve', { ids })
|
|
142
|
+
return users
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
// ── credentials (api tokens) ─────────────────────────────────────────────────
|
|
134
146
|
|
|
135
147
|
async issueToken(input: {
|
|
136
148
|
name: string
|
|
137
149
|
type?: TokenType
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
org_id?: string | null
|
|
151
|
+
user_id?: string | null
|
|
140
152
|
scopes?: string[]
|
|
141
|
-
|
|
153
|
+
expires_at?: number | null
|
|
142
154
|
metadata?: Record<string, unknown>
|
|
143
155
|
}): Promise<IssuedToken> {
|
|
144
|
-
return this.api<IssuedToken>('POST', '/
|
|
156
|
+
return this.api<IssuedToken>('POST', '/tokens', input)
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The caller's own tokens, or with `org_id` an organisation's — the latter
|
|
161
|
+
* only for a service named in the realm's IAM_ORG_TOKEN_ISSUERS.
|
|
162
|
+
*/
|
|
163
|
+
async listTokens(opts: { org_id?: string } = {}): Promise<ApiToken[]> {
|
|
164
|
+
const q = opts.org_id ? `?${new URLSearchParams({ org_id: opts.org_id })}` : ''
|
|
165
|
+
const { tokens } = await this.api<{ tokens: ApiToken[] }>('GET', `/tokens${q}`)
|
|
149
166
|
return tokens
|
|
150
167
|
}
|
|
151
168
|
|
|
152
169
|
async revokeToken(id: string): Promise<void> {
|
|
153
|
-
await this.api('DELETE', `/
|
|
170
|
+
await this.api('DELETE', `/tokens/${encodeURIComponent(id)}`)
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
/** Verify a raw token secret → its principal, or throw IamError(401). */
|
|
157
174
|
async verifyToken(secret: string): Promise<TokenPrincipal> {
|
|
158
|
-
const { principal } = await this.api<{ principal: TokenPrincipal }>('POST', '/
|
|
175
|
+
const { principal } = await this.api<{ principal: TokenPrincipal }>('POST', '/tokens/verify', { token: secret })
|
|
159
176
|
return principal
|
|
160
177
|
}
|
|
161
178
|
}
|
package/src/types.ts
CHANGED
|
@@ -34,7 +34,11 @@ export interface ApiTokenRow {
|
|
|
34
34
|
updated_at: number
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// ── REST DTOs
|
|
37
|
+
// ── REST DTOs ─────────────────────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// snake_case on the wire (ADR-043), matching the row names above. The mapping
|
|
40
|
+
// from row to DTO now only does real work — parsing JSON columns, hiding the
|
|
41
|
+
// hash — instead of also renaming every field.
|
|
38
42
|
|
|
39
43
|
export interface User {
|
|
40
44
|
id: string
|
|
@@ -43,8 +47,8 @@ export interface User {
|
|
|
43
47
|
email: string
|
|
44
48
|
name: string | null
|
|
45
49
|
picture: string | null
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
created_at: number
|
|
51
|
+
updated_at: number
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Public-safe identity projection (returned by /me and /users/lookup). */
|
|
@@ -67,16 +71,16 @@ export interface OidcIdentity {
|
|
|
67
71
|
export interface ApiToken {
|
|
68
72
|
id: string
|
|
69
73
|
type: TokenType
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
org_id: string | null
|
|
75
|
+
user_id: string | null
|
|
72
76
|
name: string
|
|
73
|
-
|
|
77
|
+
key_prefix: string
|
|
74
78
|
scopes: string[]
|
|
75
79
|
metadata: Record<string, unknown>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
expires_at: number | null
|
|
81
|
+
last_used_at: number | null
|
|
82
|
+
created_at: number
|
|
83
|
+
updated_at: number
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/** The one-time issue response — `token` is the secret, shown only here. */
|
|
@@ -88,9 +92,19 @@ export interface IssuedToken {
|
|
|
88
92
|
|
|
89
93
|
/** The identity a verified token resolves to. */
|
|
90
94
|
export interface TokenPrincipal {
|
|
91
|
-
|
|
95
|
+
token_id: string
|
|
92
96
|
type: TokenType
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
org_id: string | null
|
|
98
|
+
user_id: string | null
|
|
95
99
|
scopes: string[]
|
|
100
|
+
/** The name it was issued under, e.g. `notes-api@staging`. */
|
|
101
|
+
name: string
|
|
102
|
+
/**
|
|
103
|
+
* What the issuer bound the token to, returned as it was stored. A service
|
|
104
|
+
* that issues tokens for its own workloads keeps the binding here (for
|
|
105
|
+
* example the scope and app a runtime token belongs to) and checks it on
|
|
106
|
+
* every verify, because iam keeps it opaque and never interprets it.
|
|
107
|
+
*/
|
|
108
|
+
metadata: Record<string, unknown>
|
|
109
|
+
expires_at: number | null
|
|
96
110
|
}
|