@nauticana/sail 1.1.36 → 1.1.37
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/README.md +4 -4
- package/package.json +1 -1
- package/src/agent/component/approval_inbox.html +18 -14
- package/src/agent/component/approval_inbox.ts +19 -12
- package/src/agent/model/agent_studio.ts +3 -1
- package/src/agent/model/approval.ts +1 -1
- package/src/agent/service/approval.service.ts +3 -3
- package/src/component/security/api_keys.html +2 -2
- package/src/component/security/api_keys.ts +15 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A shared Angular component library for building CRUD-based admin frontends. Provides table management, form handling, navigation, authentication, two-factor authentication, and trusted device management — all driven by metadata from a [keel](https://github.com/nauticana/keel) Go backend.
|
|
4
4
|
|
|
5
|
-
> **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.36 adopts keel v1.2.113 (API key expiry, caller-aware scope policy), sail v1.1.35 adopts keel v1.2.112 (minor-unit money, the `currency` catalog, OAuth grant routes), sail v1.1.33 adopts keel v1.2.106 (`prompt` on the login page, new-device step-up, sign-in sessions), sail v1.1.32 adopts keel v1.2.101 (the OAuth connect callback ticket), sail v1.1.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), sail v1.1.28 adopts keel v1.2.93 (absolute `return` for the OAuth hand-off), sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). Earlier lines: **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**, **sail v0.9.x ↔ keel v0.9.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
|
|
5
|
+
> **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.37 adopts keel v1.2.114 (scopes validated against a constant list), sail v1.1.36 adopts keel v1.2.113 (API key expiry, caller-aware scope policy), sail v1.1.35 adopts keel v1.2.112 (minor-unit money, the `currency` catalog, OAuth grant routes), sail v1.1.33 adopts keel v1.2.106 (`prompt` on the login page, new-device step-up, sign-in sessions), sail v1.1.32 adopts keel v1.2.101 (the OAuth connect callback ticket), sail v1.1.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), sail v1.1.28 adopts keel v1.2.93 (absolute `return` for the OAuth hand-off), sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). Earlier lines: **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**, **sail v0.9.x ↔ keel v0.9.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
|
|
6
6
|
|
|
7
7
|
**Recent additions:** `BaseAuthService.acceptToken(jwt)` — adopt an externally-minted JWT (registration / SSO-handoff flows) and run the full post-login sequence (store under the canonical `jwt` key, load appdata, init routes); apps must use this instead of writing `localStorage` directly. `BaseRestService.analytic<T>(endpoint, params?)` — GET a keel `analytic/<endpoint>` report and return its rows. `passwordPolicyValidator` + `BaseAuthService.ensurePasswordPolicy()` — validate passwords against keel's policy (via `SailGuiConfig.passwordPolicyUrl`); see Configuration reference.
|
|
8
8
|
|
|
@@ -414,7 +414,7 @@ The login flow redirects `twoFactorRequired` responses to `/login/2fa`. With `tw
|
|
|
414
414
|
|
|
415
415
|
`SessionsComponent` (`<sail-sessions>`) lists the sessions with their `sign_in_method` captions, marks the current one, and signs out any other.
|
|
416
416
|
|
|
417
|
-
`ApiKeysComponent` (`<sail-api-keys [scopes]>`) lists the partner's keys and uses the backend-provided `generate` and `restrict` table actions; controls appear only for granted actions.
|
|
417
|
+
`ApiKeysComponent` (`<sail-api-keys [scopes] [scopeDomains]>`) lists the partner's keys and uses the backend-provided `generate` and `restrict` table actions; controls appear only for granted actions. Offer scopes as `LabelOption[]` or as constant domains whose values the component loads, leaving out those the caller may not mint; with none, keel's `ScopePolicy` supplies them. A key may carry an expiry, shown in its own column. The role also needs generic `SELECT` and `DELETE` grants to list and revoke keys. `ConnectedAppsComponent` (`<sail-connected-apps>`) lists and disconnects OAuth clients through keel's `GrantHandler` routes (v1.2.112, `RestURL.oauthGrantsURL` and `oauthGrantRevokeURL`).
|
|
418
418
|
|
|
419
419
|
### Cross-origin cookie requirement
|
|
420
420
|
|
|
@@ -689,7 +689,7 @@ A complete screen is the pieces above assembled over `BillingService` — no met
|
|
|
689
689
|
|
|
690
690
|
## Agent UI (v1.1.20)
|
|
691
691
|
|
|
692
|
-
Agent Studio and agent-conversation UI live in a secondary entry point, `@nauticana/sail/agent`. Nothing in it is exported from `@nauticana/sail`, so an app that never imports it bundles none of it. It needs no additional peer dependency. The wire types match Scout v1.2.
|
|
692
|
+
Agent Studio and agent-conversation UI live in a secondary entry point, `@nauticana/sail/agent`. Nothing in it is exported from `@nauticana/sail`, so an app that never imports it bundles none of it. It needs no additional peer dependency. The wire types match Scout v1.2.16.
|
|
693
693
|
|
|
694
694
|
```ts
|
|
695
695
|
import { AgentStudioService, PromptLanguageEditorComponent, TurnStream } from '@nauticana/sail/agent';
|
|
@@ -702,7 +702,7 @@ import { AgentStudioService, PromptLanguageEditorComponent, TurnStream } from '@
|
|
|
702
702
|
| `sail-studio-page` Guardrails tab | Shown when the `features` manifest lists `guardrails`: inherited layers read-only, the agent's own rules as an editable JSON array saved against `expected_agent_revision`, and its safety events paged by `before`. Edit needs the feature's `EDIT` action. The rail captions Scout's fixed `AgentReadiness` values (`AGENT_READINESS_CAPTIONS`). |
|
|
703
703
|
| `sail-assistant-activity` | The tenant's decision records from Scout's `decisions` feature (`GET /api/agent-studio/decisions`, `AGENT_AUDIT` `VIEW`), newest first and paged by `next_before`: when, action, resource, outcome caption, who, app, and whom it acted for, named by the product's Scout `Labeler` (kind and id, or the client ref, without one). `[category]` and `[outcome]` filter. |
|
|
704
704
|
| `sail-turn-thread` | The signed-in person's turns from `GET {base}turns` (`ConversationService.turns`), newest first, paged by `next_before` and optionally narrowed to `[workRef]`: `turn_status` caption, settled cost in minor units with its exponent, agent and release version. Each `task_kind` renders its result through `<ng-template sailTurnResult="kind" let-turn>` (`"*"` for any other kind; `let-turn` is a `ConversationTurn`); a failed turn without error text shows `[failedText]`; the first page reloads every `[pollMs]` while a turn is nonterminal. |
|
|
705
|
-
| `sail-approval-inbox` / `ApprovalService` | The signed-in person's pending approvals (`GET /api/approval/pending`) and MCP confirmations (`GET /api/approval/confirmations`) grouped as waiting, submitted, needing reconciliation and recent; each list loads independently and a `404` leaves it empty. Approvals are approved, rejected, or edited (`POST /api/approval/resolve`); confirmations are confirmed, declined, withdrawn, or reconciled as executed or failed through `/api/approval/confirmation/*`, each control shown only when Scout says the person may. Approving
|
|
705
|
+
| `sail-approval-inbox` / `ApprovalService` | The signed-in person's pending approvals (`GET /api/approval/pending`) and MCP confirmations (`GET /api/approval/confirmations`) grouped as waiting, submitted, needing reconciliation and recent; each list loads independently and a `404` leaves it empty. Approvals are approved, rejected, or edited (`POST /api/approval/resolve`); confirmations are confirmed, declined, withdrawn, or reconciled as executed or failed through `/api/approval/confirmation/*`, each control shown only when Scout says the person may. Approving and withdrawing ask once more; reconciling requires a note of up to 500 characters. `[open]` selects an item by key or confirmation id (an emailed link); `[summary]` names a confirmation in the list and heading (default its tool); `[historyDays]` (30) and `[limit]` (200) bound the closed ones listed by creation time and count. People, workspaces and apps show their Scout `Labeler` captions. A tool's preview renders through `<ng-template sailApprovalPreview="tool" let-c>` (`"*"` for any other tool), otherwise as JSON. A `404` or `409` reloads the list; `(decided)` and `(confirmationChanged)` emit the result. |
|
|
706
706
|
| `studioErrorMessage(err)` | One message for a failed Studio call: the conflict cause, the field errors, or the detail. |
|
|
707
707
|
| `studioConflictCause(err)` | `'revision'` (reload the draft), `'sealed'` (a layer sits under a sealed one) or `'other'` for a `409`. |
|
|
708
708
|
| `studioFieldErrors(err)` / `retryAfterSeconds(err)` | Field errors of a `400`; `Retry-After` of a `429`/`503`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.37",
|
|
4
4
|
"description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<li>
|
|
20
20
|
<button type="button" class="approval-inbox__item" [class.selected]="confirmationKey(c) === selectedKey()"
|
|
21
21
|
[attr.aria-current]="confirmationKey(c) === selectedKey()" (click)="select(confirmationKey(c))">
|
|
22
|
-
<span class="approval-inbox__summary">{{ c
|
|
22
|
+
<span class="approval-inbox__summary">{{ summary()(c) }} · {{ c.status | sailCaption:'mcp_confirmation_status' }}</span>
|
|
23
23
|
@if (c.status === 'P') {
|
|
24
24
|
<span class="approval-inbox__deadline">Expires {{ c.expires_at | date: 'medium' }}</span>
|
|
25
25
|
}
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
@if (confirming()) {
|
|
69
69
|
<span>Approve this action? It runs exactly as shown.</span>
|
|
70
70
|
<button #yes matButton="filled" type="button" [disabled]="loading()" (click)="decide('A')">Yes, approve</button>
|
|
71
|
-
<button matButton type="button" [disabled]="loading()" (click)="
|
|
71
|
+
<button matButton type="button" [disabled]="loading()" (click)="cancelAsk()">Cancel</button>
|
|
72
72
|
} @else {
|
|
73
|
-
<button matButton="filled" type="button" [disabled]="loading()" (click)="
|
|
73
|
+
<button matButton="filled" type="button" [disabled]="loading()" (click)="ask('approve')">Approve</button>
|
|
74
74
|
<button matButton type="button" class="warn" [disabled]="loading()" (click)="decide('X')">Reject</button>
|
|
75
75
|
<button matButton type="button" [disabled]="loading() || !revisedArguments().trim()" (click)="decide('E')">Edit and run</button>
|
|
76
76
|
}
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
|
|
81
81
|
@if (selectedConfirmation(); as c) {
|
|
82
82
|
<section class="approval-inbox__decision" aria-label="Confirmation">
|
|
83
|
-
<h3 #heading tabindex="-1">{{ c
|
|
83
|
+
<h3 #heading tabindex="-1">{{ summary()(c) }}</h3>
|
|
84
84
|
<dl>
|
|
85
85
|
<dt>Workspace</dt><dd>{{ c.tenant_label || c.tenant_id }}</dd>
|
|
86
86
|
<dt>Status</dt><dd>{{ c.status | sailCaption:'mcp_confirmation_status' }}@if (c.expired) { (expired) }</dd>
|
|
@@ -106,18 +106,22 @@
|
|
|
106
106
|
</mat-form-field>
|
|
107
107
|
}
|
|
108
108
|
<div class="approval-inbox__actions" role="group" aria-label="Decide">
|
|
109
|
-
@if (c.can_decide) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
@if (c.can_decide && confirming() === 'approve') {
|
|
110
|
+
<span>Confirm this action? It runs exactly as shown.</span>
|
|
111
|
+
<button #yes matButton="filled" type="button" [disabled]="loading()" (click)="confirm(true)">Yes, confirm</button>
|
|
112
|
+
<button matButton type="button" [disabled]="loading()" (click)="cancelAsk()">Cancel</button>
|
|
113
|
+
} @else if (c.can_withdraw && confirming() === 'withdraw') {
|
|
114
|
+
<span>Withdraw this confirmation? Nothing will be done.</span>
|
|
115
|
+
<button #yes matButton="filled" type="button" [disabled]="loading()" (click)="withdraw()">Yes, withdraw</button>
|
|
116
|
+
<button matButton type="button" [disabled]="loading()" (click)="cancelAsk()">Cancel</button>
|
|
117
|
+
} @else {
|
|
118
|
+
@if (c.can_decide) {
|
|
119
|
+
<button matButton="filled" type="button" [disabled]="loading()" (click)="ask('approve')">Confirm</button>
|
|
116
120
|
<button matButton type="button" class="warn" [disabled]="loading()" (click)="confirm(false)">Decline</button>
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
@if (c.can_withdraw) {
|
|
123
|
+
<button matButton type="button" [disabled]="loading()" (click)="ask('withdraw')">Withdraw</button>
|
|
124
|
+
}
|
|
121
125
|
}
|
|
122
126
|
@if (c.can_reconcile) {
|
|
123
127
|
<button matButton type="button" [disabled]="loading() || !reason().trim()" (click)="reconcile('D')">Mark executed</button>
|
|
@@ -49,8 +49,8 @@ const actionErrors: Record<number, string> = {
|
|
|
49
49
|
/**
|
|
50
50
|
* The signed-in person's pending approvals and MCP confirmations, grouped as waiting, submitted,
|
|
51
51
|
* needing reconciliation and recent, with a decision view: approve, reject, or edit a tool call's
|
|
52
|
-
* arguments; confirm, decline, withdraw, or reconcile a confirmation. Approving
|
|
53
|
-
* reconciling requires a note. A list the backend does not serve (404) stays empty.
|
|
52
|
+
* arguments; confirm, decline, withdraw, or reconcile a confirmation. Approving and withdrawing ask
|
|
53
|
+
* once more, and reconciling requires a note. A list the backend does not serve (404) stays empty.
|
|
54
54
|
* Selector: <sail-approval-inbox>.
|
|
55
55
|
*/
|
|
56
56
|
@Component({
|
|
@@ -69,6 +69,12 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
69
69
|
|
|
70
70
|
/** The item to open first: an approval or confirmation key, or a confirmation id from a link. */
|
|
71
71
|
readonly open = input('');
|
|
72
|
+
/** How a confirmation reads in the list and as its heading. */
|
|
73
|
+
readonly summary = input<(c: Confirmation) => string>((c) => c.tool);
|
|
74
|
+
/** Days back a closed confirmation may have been created and still list; open ones always list. */
|
|
75
|
+
readonly historyDays = input(30);
|
|
76
|
+
/** Most confirmations listed, newest first. */
|
|
77
|
+
readonly limit = input(200);
|
|
72
78
|
|
|
73
79
|
readonly decided = output<ApprovalRequest>();
|
|
74
80
|
readonly confirmationChanged = output<Confirmation>();
|
|
@@ -81,8 +87,8 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
81
87
|
readonly selectedKey = signal('');
|
|
82
88
|
readonly reason = signal('');
|
|
83
89
|
readonly revisedArguments = signal('');
|
|
84
|
-
/** The
|
|
85
|
-
readonly confirming = signal(
|
|
90
|
+
/** The step awaiting its second click. */
|
|
91
|
+
readonly confirming = signal<'' | 'approve' | 'withdraw'>('');
|
|
86
92
|
readonly selected = computed(() => this.requests().find((r) => this.key(r) === this.selectedKey()));
|
|
87
93
|
readonly selectedConfirmation = computed(() => this.confirmations().find((c) => this.confirmationKey(c) === this.selectedKey()));
|
|
88
94
|
|
|
@@ -93,7 +99,8 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
93
99
|
load(): void {
|
|
94
100
|
this.run(forkJoin([
|
|
95
101
|
this.listed(this.approvals.pending(), this.requests(), 'Failed to load approvals.'),
|
|
96
|
-
this.listed(this.approvals.confirmations()
|
|
102
|
+
this.listed(this.approvals.confirmations(new Date(Date.now() - this.historyDays() * 86_400_000).toISOString(), this.limit()),
|
|
103
|
+
this.confirmations(), 'Failed to load confirmations.'),
|
|
97
104
|
]), ([requests, confirmations]) => {
|
|
98
105
|
this.requests.set(requests);
|
|
99
106
|
this.confirmations.set(confirmations);
|
|
@@ -121,16 +128,16 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
121
128
|
this.selectedKey.set(key);
|
|
122
129
|
this.reason.set('');
|
|
123
130
|
this.revisedArguments.set('');
|
|
124
|
-
this.confirming.set(
|
|
131
|
+
this.confirming.set('');
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
this.confirming.set(
|
|
134
|
+
ask(step: 'approve' | 'withdraw'): void {
|
|
135
|
+
this.confirming.set(step);
|
|
129
136
|
this.focus(this.yes);
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
this.confirming.set(
|
|
139
|
+
cancelAsk(): void {
|
|
140
|
+
this.confirming.set('');
|
|
134
141
|
this.focus(this.heading);
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -174,7 +181,7 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
174
181
|
|
|
175
182
|
withdraw(): void {
|
|
176
183
|
const c = this.selectedConfirmation();
|
|
177
|
-
if (!c
|
|
184
|
+
if (!c) return;
|
|
178
185
|
this.act(this.approvals.withdrawConfirmation(c), () => {
|
|
179
186
|
this.load();
|
|
180
187
|
return 'Withdrawn; nothing will be done.';
|
|
@@ -207,7 +214,7 @@ export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
|
207
214
|
private act<T>(request: Observable<T>, done: (result: T) => string): void {
|
|
208
215
|
this.run(request, (result) => {
|
|
209
216
|
this.reason.set('');
|
|
210
|
-
this.confirming.set(
|
|
217
|
+
this.confirming.set('');
|
|
211
218
|
this.successMessage.set(done(result));
|
|
212
219
|
this.focus(this.heading);
|
|
213
220
|
}, 'Failed to record the decision.', (err) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Agent Studio wire types (Scout v1.2.
|
|
1
|
+
/** Agent Studio wire types (Scout v1.2.16). Field names are the backend's JSON names. */
|
|
2
2
|
|
|
3
3
|
/** R replace, A append, I intersect (non-prompt settings only). */
|
|
4
4
|
export type AgentMergeMode = 'R' | 'A' | 'I';
|
|
@@ -333,6 +333,8 @@ export interface GuardrailRule {
|
|
|
333
333
|
action: string;
|
|
334
334
|
severity: string;
|
|
335
335
|
params?: unknown;
|
|
336
|
+
/** Why the rule exists; a blocked answer names it. */
|
|
337
|
+
reason?: string;
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
/** Rules an agent inherits from one layer: B baseline, P platform, T tenant, Y type. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PrincipalRef } from './decision';
|
|
2
2
|
|
|
3
|
-
/** Scout approval desk and MCP confirmation inbox wire types (v1.2.
|
|
3
|
+
/** Scout approval desk and MCP confirmation inbox wire types (v1.2.16). Codes render through backend captions. */
|
|
4
4
|
|
|
5
5
|
/** A approved, X rejected, E edited (tool-call proposals only, with revised arguments). */
|
|
6
6
|
export type ApprovalVerdict = 'A' | 'X' | 'E';
|
|
@@ -18,9 +18,9 @@ export class ApprovalService extends BaseRestService {
|
|
|
18
18
|
return this.http.post<ApprovalRequest>(this.url(base + 'resolve'), request);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
confirmations(since = ''): Observable<Confirmation[]> {
|
|
23
|
-
return this.http.get<Confirmation[]>(this.url(base + 'confirmations'), { params:
|
|
21
|
+
/** Lists open confirmations plus those since the RFC 3339 time; zero limit takes Scout's batch. */
|
|
22
|
+
confirmations(since = '', limit = 0): Observable<Confirmation[]> {
|
|
23
|
+
return this.http.get<Confirmation[]>(this.url(base + 'confirmations'), { params: { since, limit } });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
decideConfirmation(key: ConfirmationKey, approve: boolean, note = ''): Observable<Confirmation> {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<mat-label>Expires (optional)</mat-label>
|
|
23
23
|
<input matInput type="datetime-local" formControlName="expiresAt">
|
|
24
24
|
</mat-form-field>
|
|
25
|
-
@if (
|
|
25
|
+
@if (offered().length) {
|
|
26
26
|
<fieldset class="api-keys__scopes">
|
|
27
27
|
<legend>Scopes</legend>
|
|
28
|
-
@for (scope of
|
|
28
|
+
@for (scope of offered(); track scope.code) {
|
|
29
29
|
<mat-checkbox [checked]="form.controls.scopes.value.includes(scope.code)"
|
|
30
30
|
(change)="toggleScope(scope.code, $event.checked)">{{ scope.caption }}</mat-checkbox>
|
|
31
31
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { DatePipe } from '@angular/common';
|
|
2
|
-
import { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation, inject, input, signal } from '@angular/core';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation, computed, inject, input, signal } from '@angular/core';
|
|
3
3
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
4
4
|
import { MatButtonModule } from '@angular/material/button';
|
|
5
5
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
6
6
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
7
7
|
import { MatInputModule } from '@angular/material/input';
|
|
8
|
+
import { forkJoin } from 'rxjs';
|
|
8
9
|
import { ApiKey, MintedApiKey } from '../../model/access';
|
|
9
10
|
import { AccessService } from '../../service/access.service';
|
|
10
|
-
import { LabelOption } from '../../service/label.service';
|
|
11
|
+
import { LabelOption, LabelService } from '../../service/label.service';
|
|
11
12
|
import { BaseAsync } from '../abstract/base_async';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The partner's API keys: mint one with scopes, allowed networks and an optional expiry (the
|
|
15
|
-
* plaintext shows once), restrict its networks later, or revoke it. The app
|
|
16
|
-
*
|
|
16
|
+
* plaintext shows once), restrict its networks later, or revoke it. The app offers scopes as options
|
|
17
|
+
* or constant domains; keel's `ScopePolicy` decides and may supply them. Selector: <sail-api-keys>.
|
|
17
18
|
*/
|
|
18
19
|
@Component({
|
|
19
20
|
selector: 'sail-api-keys',
|
|
@@ -24,10 +25,15 @@ import { BaseAsync } from '../abstract/base_async';
|
|
|
24
25
|
})
|
|
25
26
|
export class ApiKeysComponent extends BaseAsync implements OnInit {
|
|
26
27
|
private readonly access = inject(AccessService);
|
|
28
|
+
private readonly labels = inject(LabelService);
|
|
27
29
|
private readonly fb = inject(FormBuilder).nonNullable;
|
|
28
30
|
|
|
29
31
|
/** Scopes offered for a new key, as code and caption. */
|
|
30
32
|
readonly scopes = input<LabelOption[]>([]);
|
|
33
|
+
/** Constant domains whose values are offered as scopes after `scopes`. */
|
|
34
|
+
readonly scopeDomains = input<string[]>([]);
|
|
35
|
+
private readonly domainScopes = signal<LabelOption[]>([]);
|
|
36
|
+
readonly offered = computed(() => [...this.scopes(), ...this.domainScopes()]);
|
|
31
37
|
|
|
32
38
|
readonly canGenerate = this.access.canApiKeyAction('generate');
|
|
33
39
|
readonly canRestrict = this.access.canApiKeyAction('restrict');
|
|
@@ -49,7 +55,11 @@ export class ApiKeysComponent extends BaseAsync implements OnInit {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
load(): void {
|
|
52
|
-
|
|
58
|
+
const domains = [...new Set(this.scopeDomains().map((d) => d.trim()))];
|
|
59
|
+
this.run(forkJoin([this.access.apiKeys(), ...domains.map((d) => this.labels.loadLabels(d))]), ([rows]) => {
|
|
60
|
+
this.keys.set(rows);
|
|
61
|
+
this.domainScopes.set(domains.flatMap((d) => this.labels.options(d)));
|
|
62
|
+
}, 'Failed to load API keys.');
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
toggleScope(code: string, checked: boolean): void {
|