@fuzdev/fuz_app 0.114.0 → 0.115.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/auth/CLAUDE.md +52 -8
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +4 -3
- package/dist/auth/audit_log_schema.d.ts +3 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +13 -3
- package/dist/auth/cell_action_specs.js +1 -1
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +33 -4
- package/dist/auth/cell_authorize.d.ts +3 -0
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +7 -4
- package/dist/auth/request_context.d.ts +12 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +12 -3
- package/dist/auth/role_grant_offer_action_specs.d.ts +30 -5
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +53 -17
- package/dist/auth/role_grant_offer_actions.d.ts +23 -12
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +132 -38
- package/dist/auth/role_grant_offer_queries.d.ts +36 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +76 -0
- package/dist/auth/role_schema.d.ts +12 -0
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +12 -0
- package/dist/auth/self_service_role_actions.d.ts +14 -0
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +25 -1
- package/dist/server/serve_fact_route.d.ts +3 -1
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -4
- package/dist/testing/CLAUDE.md +12 -4
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +23 -1
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +34 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +12 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +13 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +9 -7
- package/dist/testing/entities.d.ts +6 -3
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -1
- package/dist/ui/BootstrapForm.svelte +14 -10
- package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +19 -7
- package/dist/ui/LoginForm.svelte +8 -4
- package/dist/ui/LoginForm.svelte.d.ts.map +1 -1
- package/dist/ui/RoleGrantOfferForm.svelte +29 -14
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts +2 -3
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -1
- package/dist/ui/SignupForm.svelte +14 -13
- package/dist/ui/form_state.svelte.d.ts +15 -7
- package/dist/ui/form_state.svelte.d.ts.map +1 -1
- package/dist/ui/form_state.svelte.js +10 -2
- package/dist/ui/role_grant_offers_state.svelte.d.ts +25 -15
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +7 -1
- package/package.json +1 -21
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
|
|
18
18
|
import { autofocus } from '@fuzdev/fuz_ui/autofocus.svelte.ts';
|
|
19
19
|
|
|
20
|
-
import { Username } from '../primitive_schemas.js';
|
|
20
|
+
import { Username, USERNAME_LENGTH_MAX, USERNAME_LENGTH_MIN } from '../primitive_schemas.js';
|
|
21
21
|
import { PASSWORD_LENGTH_MIN } from '../auth/password.js';
|
|
22
22
|
import { auth_state_context } from './auth_state.svelte.js';
|
|
23
23
|
import { FormState } from './form_state.svelte.js';
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
} = $props();
|
|
34
34
|
|
|
35
35
|
const auth_state = auth_state_context.get();
|
|
36
|
-
const form_state = new FormState();
|
|
36
|
+
const form_state = new FormState<'token' | 'username' | 'password' | 'password_confirm'>();
|
|
37
37
|
|
|
38
38
|
let token = $state.raw('');
|
|
39
39
|
let username = $state.raw('');
|
|
@@ -47,11 +47,13 @@
|
|
|
47
47
|
username.trim() &&
|
|
48
48
|
username_valid &&
|
|
49
49
|
password.length >= PASSWORD_LENGTH_MIN &&
|
|
50
|
-
passwords_match
|
|
51
|
-
!auth_state.verifying
|
|
50
|
+
passwords_match
|
|
52
51
|
);
|
|
53
52
|
|
|
54
53
|
const handle_bootstrap = async (): Promise<void> => {
|
|
54
|
+
// while verifying the fields are `readonly` and the button `aria-disabled` —
|
|
55
|
+
// `disabled` would drop focus to `<body>` — so Enter and clicks still land here
|
|
56
|
+
if (auth_state.verifying) return;
|
|
55
57
|
form_state.attempt();
|
|
56
58
|
if (!can_submit) {
|
|
57
59
|
if (!token.trim()) form_state.focus('token');
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
type="password"
|
|
87
89
|
bind:value={token}
|
|
88
90
|
placeholder="paste token"
|
|
89
|
-
|
|
91
|
+
readonly={auth_state.verifying}
|
|
90
92
|
{@attach autofocus()}
|
|
91
93
|
/>
|
|
92
94
|
</label>
|
|
@@ -98,12 +100,13 @@
|
|
|
98
100
|
bind:value={username}
|
|
99
101
|
placeholder="admin"
|
|
100
102
|
autocomplete="username"
|
|
101
|
-
|
|
103
|
+
readonly={auth_state.verifying}
|
|
102
104
|
/>
|
|
103
105
|
</label>
|
|
104
106
|
{#if form_state.show('username') && username && !username_valid}
|
|
105
107
|
<p class="color_c_50 font_size_sm mt_0 mb_xs">
|
|
106
|
-
|
|
108
|
+
{USERNAME_LENGTH_MIN}-{USERNAME_LENGTH_MAX} chars, starts with a letter, ends with
|
|
109
|
+
letter/number, middle allows dash/underscore
|
|
107
110
|
</p>
|
|
108
111
|
{/if}
|
|
109
112
|
<fieldset>
|
|
@@ -115,7 +118,7 @@
|
|
|
115
118
|
bind:value={password}
|
|
116
119
|
placeholder="password"
|
|
117
120
|
autocomplete="new-password"
|
|
118
|
-
|
|
121
|
+
readonly={auth_state.verifying}
|
|
119
122
|
/>
|
|
120
123
|
</label>
|
|
121
124
|
{#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
|
|
@@ -131,7 +134,7 @@
|
|
|
131
134
|
bind:value={password_confirm}
|
|
132
135
|
placeholder="confirm password"
|
|
133
136
|
autocomplete="new-password"
|
|
134
|
-
|
|
137
|
+
readonly={auth_state.verifying}
|
|
135
138
|
/>
|
|
136
139
|
</label>
|
|
137
140
|
{#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
|
|
@@ -141,7 +144,8 @@
|
|
|
141
144
|
<div class="row gap_sm">
|
|
142
145
|
<PendingButton
|
|
143
146
|
pending={auth_state.verifying}
|
|
144
|
-
disabled={
|
|
147
|
+
disabled={false}
|
|
148
|
+
aria-disabled={auth_state.verifying}
|
|
145
149
|
onclick={handle_bootstrap}
|
|
146
150
|
class={auth_state.verify_error ? 'color_c' : ''}
|
|
147
151
|
>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAyBC,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAyBC,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AA0GH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
package/dist/ui/CLAUDE.md
CHANGED
|
@@ -93,7 +93,12 @@ pattern rather than reintroducing prop-drilling.
|
|
|
93
93
|
## Auth forms
|
|
94
94
|
|
|
95
95
|
All four consume `auth_state_context.get()`; all three form-driven ones
|
|
96
|
-
attach a `FormState` for Enter-advance + blur-touched validation.
|
|
96
|
+
attach a `FormState` for Enter-advance + blur-touched validation. While a
|
|
97
|
+
submit is in flight their fields are `readonly` and the `PendingButton` is
|
|
98
|
+
`aria-disabled` (with `disabled={false}` overriding its disable-while-pending
|
|
99
|
+
default), never `disabled` — disabling the focused element drops focus to
|
|
100
|
+
`<body>`. So the handler guards re-entry itself (`if (auth_state.verifying)
|
|
101
|
+
return;`), since Enter and clicks still reach it.
|
|
97
102
|
|
|
98
103
|
- `LoginForm.svelte` — props `username_label = 'username or email'`,
|
|
99
104
|
`redirect_on_login`. Clears `auth_state.verify_error` on input.
|
|
@@ -170,13 +175,18 @@ destructive actions.
|
|
|
170
175
|
to a specific actor on the recipient account; default account-grain),
|
|
171
176
|
`roles: Array<string>` (pre-filtered upstream by admin-grant-path —
|
|
172
177
|
`RoleSpec.grant_paths` includes `'admin'`),
|
|
173
|
-
`
|
|
174
|
-
|
|
178
|
+
`scope_kind` + `scope_id` (`RoleGrantOfferScope` — together, or neither
|
|
179
|
+
for a global offer; the prop types refuse one alone), `on_created?`,
|
|
180
|
+
`format_role?`. Surfaces the RPC error reasons with friendly copy:
|
|
181
|
+
`ERROR_ROLE_GRANT_OFFER_SELF_TARGET`,
|
|
175
182
|
`ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE`, `ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED`,
|
|
183
|
+
`ERROR_ROLE_GRANT_BUILTIN_SCOPED`,
|
|
176
184
|
`ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH`, `ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH`
|
|
177
|
-
— imported from `auth/role_grant_offer_action_specs.
|
|
185
|
+
— imported from `auth/role_grant_offer_action_specs.ts` (see
|
|
178
186
|
`auth/CLAUDE.md` for `role_grant_offer_action_specs.ts` +
|
|
179
|
-
`role_grant_offer_actions.ts`).
|
|
187
|
+
`role_grant_offer_actions.ts`). Submits like the auth forms (`readonly`
|
|
188
|
+
textarea, `aria-disabled` button, re-entry guard); its select stays
|
|
189
|
+
`disabled`, having no `readonly` — safe, since the button has focus.
|
|
180
190
|
- `RoleGrantOfferHistory.svelte` — both-directions history (recipient +
|
|
181
191
|
grantor, including terminal). Props: `current_actor_id: string | null`
|
|
182
192
|
(classifies row as "sent" vs "received"), `format_actor?`,
|
|
@@ -259,8 +269,10 @@ destructive actions.
|
|
|
259
269
|
Methods: `form()` (returns a Svelte `Attachment` for the form
|
|
260
270
|
element), `show(field)` (touched OR attempted), `is_touched(field)`,
|
|
261
271
|
`touch(field)` (programmatic), `focus(field)` (queries by `name`),
|
|
262
|
-
`attempt()`, `reset()`.
|
|
263
|
-
|
|
272
|
+
`attempt()`, `reset()`. Generic over its field names —
|
|
273
|
+
`new FormState<'username' | 'password'>()` makes a misspelled field a
|
|
274
|
+
compile error (defaults to `string`). In DEV throws if an input loses
|
|
275
|
+
focus without a `name` attribute — all tracked inputs must be named.
|
|
264
276
|
- `sidebar_state.svelte.ts` — see Shell + layout above.
|
|
265
277
|
|
|
266
278
|
## Per-domain state modules
|
package/dist/ui/LoginForm.svelte
CHANGED
|
@@ -34,12 +34,15 @@
|
|
|
34
34
|
} = $props();
|
|
35
35
|
|
|
36
36
|
const auth_state = auth_state_context.get();
|
|
37
|
-
const form_state = new FormState();
|
|
37
|
+
const form_state = new FormState<'username' | 'password'>();
|
|
38
38
|
|
|
39
39
|
let username = $state.raw('');
|
|
40
40
|
let password = $state.raw('');
|
|
41
41
|
|
|
42
42
|
const handle_login = async (): Promise<void> => {
|
|
43
|
+
// while verifying the fields are `readonly` and the button `aria-disabled` —
|
|
44
|
+
// `disabled` would drop focus to `<body>` — so Enter and clicks still land here
|
|
45
|
+
if (auth_state.verifying) return;
|
|
43
46
|
const u = username.trim();
|
|
44
47
|
const p = password;
|
|
45
48
|
if (!u) {
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
bind:value={username}
|
|
80
83
|
placeholder={username_label}
|
|
81
84
|
autocomplete="username"
|
|
82
|
-
|
|
85
|
+
readonly={auth_state.verifying}
|
|
83
86
|
{@attach autofocus()}
|
|
84
87
|
/>
|
|
85
88
|
</label>
|
|
@@ -91,13 +94,14 @@
|
|
|
91
94
|
bind:value={password}
|
|
92
95
|
placeholder="password"
|
|
93
96
|
autocomplete="current-password"
|
|
94
|
-
|
|
97
|
+
readonly={auth_state.verifying}
|
|
95
98
|
/>
|
|
96
99
|
</label>
|
|
97
100
|
<div class="row gap_sm">
|
|
98
101
|
<PendingButton
|
|
99
102
|
pending={auth_state.verifying}
|
|
100
|
-
disabled={
|
|
103
|
+
disabled={false}
|
|
104
|
+
aria-disabled={auth_state.verifying}
|
|
101
105
|
onclick={handle_login}
|
|
102
106
|
class={auth_state.verify_error ? 'color_c' : ''}
|
|
103
107
|
>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAmBC,KAAK,gBAAgB,GAAI;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;
|
|
1
|
+
{"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAmBC,KAAK,gBAAgB,GAAI;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAyEH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -4,22 +4,29 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Caller supplies `to_account_id`, the subset of roles the grantor may
|
|
6
6
|
* offer (typically filtered by admin-grant-path — `RoleSpec.grant_paths`
|
|
7
|
-
* includes `'admin'`), an optional `scope_id
|
|
8
|
-
* and an optional `on_created`
|
|
9
|
-
*
|
|
10
|
-
* role-not-grantable, not-authorized
|
|
7
|
+
* includes `'admin'`), an optional scope — `scope_kind` and `scope_id`
|
|
8
|
+
* together, or neither for a global offer — and an optional `on_created`
|
|
9
|
+
* callback for post-submit UX. Errors from the RPC surface their distinct
|
|
10
|
+
* reason codes (self-target, role-not-grantable, not-authorized, a scoped
|
|
11
|
+
* builtin role, actor mismatches) so consumers can render them
|
|
11
12
|
* appropriately.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
role_grant_offers_state_context,
|
|
21
|
+
type RoleGrantOfferScope
|
|
22
|
+
} from './role_grant_offers_state.svelte.js';
|
|
17
23
|
import { FormState } from './form_state.svelte.js';
|
|
18
24
|
import {
|
|
19
25
|
ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX,
|
|
20
26
|
type RoleGrantOfferJson
|
|
21
27
|
} from '../auth/role_grant_offer_schema.js';
|
|
22
28
|
import {
|
|
29
|
+
ERROR_ROLE_GRANT_BUILTIN_SCOPED,
|
|
23
30
|
ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
|
|
24
31
|
ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH,
|
|
25
32
|
ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED,
|
|
@@ -27,13 +34,15 @@
|
|
|
27
34
|
ERROR_ROLE_GRANT_OFFER_SELF_TARGET
|
|
28
35
|
} from '../auth/role_grant_offer_action_specs.js';
|
|
29
36
|
|
|
37
|
+
// `scope` is the offer's resource scope, `RoleGrantOfferScope`: `scope_kind`
|
|
38
|
+
// and `scope_id` together, or neither (the default) for a global offer
|
|
30
39
|
const {
|
|
31
40
|
to_account_id,
|
|
32
41
|
to_actor_id = null,
|
|
33
42
|
roles,
|
|
34
|
-
scope_id = null,
|
|
35
43
|
on_created,
|
|
36
|
-
format_role = (role: string) => role
|
|
44
|
+
format_role = (role: string) => role,
|
|
45
|
+
...scope
|
|
37
46
|
}: {
|
|
38
47
|
to_account_id: string;
|
|
39
48
|
/**
|
|
@@ -44,14 +53,12 @@
|
|
|
44
53
|
to_actor_id?: string | null;
|
|
45
54
|
/** Roles the caller may offer — caller filters upstream (default: admin-grant-path). */
|
|
46
55
|
roles: Array<string>;
|
|
47
|
-
/** Resource scope for the offer; `null` (default) yields a global offer. */
|
|
48
|
-
scope_id?: string | null;
|
|
49
56
|
on_created?: (offer: RoleGrantOfferJson) => void;
|
|
50
57
|
format_role?: (role: string) => string;
|
|
51
|
-
} = $props();
|
|
58
|
+
} & RoleGrantOfferScope = $props();
|
|
52
59
|
|
|
53
60
|
const role_grant_offers = role_grant_offers_state_context.get();
|
|
54
|
-
const form_state = new FormState();
|
|
61
|
+
const form_state = new FormState<'role' | 'message'>();
|
|
55
62
|
|
|
56
63
|
let role: string | undefined = $state.raw();
|
|
57
64
|
const selected_role = $derived(role ?? roles[0] ?? '');
|
|
@@ -68,6 +75,8 @@
|
|
|
68
75
|
return 'That role cannot be offered through this form.';
|
|
69
76
|
case ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED:
|
|
70
77
|
return 'You are not authorized to offer that role.';
|
|
78
|
+
case ERROR_ROLE_GRANT_BUILTIN_SCOPED:
|
|
79
|
+
return 'That role can only be offered globally, not within a scope.';
|
|
71
80
|
case ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH:
|
|
72
81
|
return 'That actor is not on the recipient account.';
|
|
73
82
|
case ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH:
|
|
@@ -78,6 +87,9 @@
|
|
|
78
87
|
};
|
|
79
88
|
|
|
80
89
|
const handle_submit = async (): Promise<void> => {
|
|
90
|
+
// while submitting the textarea is `readonly` and the button `aria-disabled` —
|
|
91
|
+
// `disabled` would drop focus to `<body>` — so clicks still land here
|
|
92
|
+
if (submitting) return;
|
|
81
93
|
form_state.attempt();
|
|
82
94
|
local_error = null;
|
|
83
95
|
if (!selected_role) {
|
|
@@ -88,7 +100,7 @@
|
|
|
88
100
|
to_account_id,
|
|
89
101
|
to_actor_id,
|
|
90
102
|
role: selected_role,
|
|
91
|
-
|
|
103
|
+
...scope,
|
|
92
104
|
message: message.trim() || null
|
|
93
105
|
});
|
|
94
106
|
if (offer) {
|
|
@@ -115,6 +127,8 @@
|
|
|
115
127
|
>
|
|
116
128
|
<label>
|
|
117
129
|
<div class="title">role</div>
|
|
130
|
+
<!-- a select has no `readonly`, but disabling it can't drop focus: the only
|
|
131
|
+
submit is the button, which has focus when the select disables -->
|
|
118
132
|
<select
|
|
119
133
|
name="role"
|
|
120
134
|
value={selected_role}
|
|
@@ -134,13 +148,14 @@
|
|
|
134
148
|
bind:value={message}
|
|
135
149
|
maxlength={ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX}
|
|
136
150
|
placeholder="optional note for the recipient"
|
|
137
|
-
|
|
151
|
+
readonly={submitting}></textarea>
|
|
138
152
|
</label>
|
|
139
153
|
|
|
140
154
|
<div class="row gap_sm">
|
|
141
155
|
<PendingButton
|
|
142
156
|
pending={submitting}
|
|
143
|
-
disabled={
|
|
157
|
+
disabled={!selected_role}
|
|
158
|
+
aria-disabled={submitting}
|
|
144
159
|
onclick={handle_submit}
|
|
145
160
|
>
|
|
146
161
|
send offer
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RoleGrantOfferScope } from './role_grant_offers_state.svelte.js';
|
|
1
2
|
import { type RoleGrantOfferJson } from '../auth/role_grant_offer_schema.js';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
to_account_id: string;
|
|
@@ -9,11 +10,9 @@ type $$ComponentProps = {
|
|
|
9
10
|
to_actor_id?: string | null;
|
|
10
11
|
/** Roles the caller may offer — caller filters upstream (default: admin-grant-path). */
|
|
11
12
|
roles: Array<string>;
|
|
12
|
-
/** Resource scope for the offer; `null` (default) yields a global offer. */
|
|
13
|
-
scope_id?: string | null;
|
|
14
13
|
on_created?: (offer: RoleGrantOfferJson) => void;
|
|
15
14
|
format_role?: (role: string) => string;
|
|
16
|
-
};
|
|
15
|
+
} & RoleGrantOfferScope;
|
|
17
16
|
declare const RoleGrantOfferForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
17
|
type RoleGrantOfferForm = ReturnType<typeof RoleGrantOfferForm>;
|
|
19
18
|
export default RoleGrantOfferForm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoleGrantOfferForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferForm.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RoleGrantOfferForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferForm.svelte"],"names":[],"mappings":"AAmBA,OAAO,EAEL,KAAK,mBAAmB,EACxB,MAAM,qCAAqC,CAAC;AAE9C,OAAO,EAEL,KAAK,kBAAkB,EACvB,MAAM,oCAAoC,CAAC;AAU5C,KAAK,gBAAgB,GAAI;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wFAAwF;IACxF,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,GAAG,mBAAmB,CAAC;AAkHzB,QAAA,MAAM,kBAAkB,sDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
|
|
17
17
|
import { autofocus } from '@fuzdev/fuz_ui/autofocus.svelte.ts';
|
|
18
18
|
|
|
19
|
-
import { Username } from '../primitive_schemas.js';
|
|
19
|
+
import { Username, USERNAME_LENGTH_MAX, USERNAME_LENGTH_MIN } from '../primitive_schemas.js';
|
|
20
20
|
import { PASSWORD_LENGTH_MIN } from '../auth/password.js';
|
|
21
21
|
import { auth_state_context } from './auth_state.svelte.js';
|
|
22
22
|
import { FormState } from './form_state.svelte.js';
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
} = $props();
|
|
33
33
|
|
|
34
34
|
const auth_state = auth_state_context.get();
|
|
35
|
-
const form_state = new FormState();
|
|
35
|
+
const form_state = new FormState<'username' | 'email' | 'password' | 'password_confirm'>();
|
|
36
36
|
|
|
37
37
|
let username = $state.raw('');
|
|
38
38
|
let email = $state.raw('');
|
|
@@ -42,14 +42,13 @@
|
|
|
42
42
|
const username_valid = $derived(Username.safeParse(username).success);
|
|
43
43
|
const passwords_match = $derived(password === password_confirm);
|
|
44
44
|
const can_submit = $derived(
|
|
45
|
-
username.trim() &&
|
|
46
|
-
username_valid &&
|
|
47
|
-
password.length >= PASSWORD_LENGTH_MIN &&
|
|
48
|
-
passwords_match &&
|
|
49
|
-
!auth_state.verifying
|
|
45
|
+
username.trim() && username_valid && password.length >= PASSWORD_LENGTH_MIN && passwords_match
|
|
50
46
|
);
|
|
51
47
|
|
|
52
48
|
const handle_signup = async (): Promise<void> => {
|
|
49
|
+
// while verifying the fields are `readonly` and the button `aria-disabled` —
|
|
50
|
+
// `disabled` would drop focus to `<body>` — so Enter and clicks still land here
|
|
51
|
+
if (auth_state.verifying) return;
|
|
53
52
|
form_state.attempt();
|
|
54
53
|
if (!can_submit) {
|
|
55
54
|
if (!username.trim() || !username_valid) form_state.focus('username');
|
|
@@ -84,13 +83,14 @@
|
|
|
84
83
|
bind:value={username}
|
|
85
84
|
placeholder="username"
|
|
86
85
|
autocomplete="username"
|
|
87
|
-
|
|
86
|
+
readonly={auth_state.verifying}
|
|
88
87
|
{@attach autofocus()}
|
|
89
88
|
/>
|
|
90
89
|
</label>
|
|
91
90
|
{#if form_state.show('username') && username && !username_valid}
|
|
92
91
|
<p class="color_c_50 font_size_sm mt_0 mb_xs">
|
|
93
|
-
|
|
92
|
+
{USERNAME_LENGTH_MIN}-{USERNAME_LENGTH_MAX} chars, starts with a letter, ends with
|
|
93
|
+
letter/number, middle allows dash/underscore
|
|
94
94
|
</p>
|
|
95
95
|
{/if}
|
|
96
96
|
<label>
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
bind:value={email}
|
|
102
102
|
placeholder="email"
|
|
103
103
|
autocomplete="email"
|
|
104
|
-
|
|
104
|
+
readonly={auth_state.verifying}
|
|
105
105
|
/>
|
|
106
106
|
</label>
|
|
107
107
|
<fieldset>
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
bind:value={password}
|
|
114
114
|
placeholder="password"
|
|
115
115
|
autocomplete="new-password"
|
|
116
|
-
|
|
116
|
+
readonly={auth_state.verifying}
|
|
117
117
|
/>
|
|
118
118
|
</label>
|
|
119
119
|
{#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
bind:value={password_confirm}
|
|
130
130
|
placeholder="confirm password"
|
|
131
131
|
autocomplete="new-password"
|
|
132
|
-
|
|
132
|
+
readonly={auth_state.verifying}
|
|
133
133
|
/>
|
|
134
134
|
</label>
|
|
135
135
|
{#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
|
|
@@ -139,7 +139,8 @@
|
|
|
139
139
|
<div class="row gap_sm">
|
|
140
140
|
<PendingButton
|
|
141
141
|
pending={auth_state.verifying}
|
|
142
|
-
disabled={
|
|
142
|
+
disabled={false}
|
|
143
|
+
aria-disabled={auth_state.verifying}
|
|
143
144
|
onclick={handle_signup}
|
|
144
145
|
class={auth_state.verify_error ? 'color_c' : ''}
|
|
145
146
|
>
|
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
* button is `type="button"`, as `PendingButton` is. Hidden inputs don't count, and an Enter that commits
|
|
13
13
|
* IME composition is left alone.
|
|
14
14
|
*
|
|
15
|
+
* While a submit is in flight, make the fields `readonly` rather than `disabled`
|
|
16
|
+
* (and the button `aria-disabled`, guarding the handler against re-entry) —
|
|
17
|
+
* disabling the focused element drops focus to `<body>`. Enter still advances
|
|
18
|
+
* through and submits from `readonly` inputs.
|
|
19
|
+
*
|
|
20
|
+
* The type parameter names the form's fields, so `focus`, `show`, `touch`,
|
|
21
|
+
* and `is_touched` reject a misspelled field at compile time.
|
|
22
|
+
*
|
|
15
23
|
* All trackable inputs must have a `name` attribute — an error is thrown in dev
|
|
16
24
|
* if an input without `name` loses focus.
|
|
17
25
|
*
|
|
18
26
|
* @example
|
|
19
27
|
* ```svelte
|
|
20
|
-
* <script>
|
|
21
|
-
* const form_state = new FormState();
|
|
28
|
+
* <script lang="ts">
|
|
29
|
+
* const form_state = new FormState<'username'>();
|
|
22
30
|
* let username = $state.raw('');
|
|
23
31
|
* const username_valid = $derived(Username.safeParse(username).success);
|
|
24
32
|
* const can_submit = $derived(username.trim() && username_valid);
|
|
@@ -45,7 +53,7 @@
|
|
|
45
53
|
* @module
|
|
46
54
|
*/
|
|
47
55
|
import type { Attachment } from 'svelte/attachments';
|
|
48
|
-
export declare class FormState {
|
|
56
|
+
export declare class FormState<TField extends string = string> {
|
|
49
57
|
#private;
|
|
50
58
|
/**
|
|
51
59
|
* Whether a submit attempt has been made.
|
|
@@ -65,22 +73,22 @@ export declare class FormState {
|
|
|
65
73
|
/**
|
|
66
74
|
* Whether a field has been blurred at least once.
|
|
67
75
|
*/
|
|
68
|
-
is_touched(field:
|
|
76
|
+
is_touched(field: TField): boolean;
|
|
69
77
|
/**
|
|
70
78
|
* Whether to show validation errors for a field.
|
|
71
79
|
* Returns `true` if the field has been blurred or a submit attempt was made.
|
|
72
80
|
*/
|
|
73
|
-
show(field:
|
|
81
|
+
show(field: TField): boolean;
|
|
74
82
|
/**
|
|
75
83
|
* Programmatically marks a field as touched without requiring a blur event.
|
|
76
84
|
*
|
|
77
85
|
* @mutates `this`
|
|
78
86
|
*/
|
|
79
|
-
touch(field:
|
|
87
|
+
touch(field: TField): void;
|
|
80
88
|
/**
|
|
81
89
|
* Focuses the named input within the form.
|
|
82
90
|
*/
|
|
83
|
-
focus(field:
|
|
91
|
+
focus(field: TField): void;
|
|
84
92
|
/**
|
|
85
93
|
* Marks the form as having been submitted, causing all field errors to show.
|
|
86
94
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUrD,qBAAa,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM;;IAKpD;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;;OASG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAmDnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAIb"}
|
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
* button is `type="button"`, as `PendingButton` is. Hidden inputs don't count, and an Enter that commits
|
|
13
13
|
* IME composition is left alone.
|
|
14
14
|
*
|
|
15
|
+
* While a submit is in flight, make the fields `readonly` rather than `disabled`
|
|
16
|
+
* (and the button `aria-disabled`, guarding the handler against re-entry) —
|
|
17
|
+
* disabling the focused element drops focus to `<body>`. Enter still advances
|
|
18
|
+
* through and submits from `readonly` inputs.
|
|
19
|
+
*
|
|
20
|
+
* The type parameter names the form's fields, so `focus`, `show`, `touch`,
|
|
21
|
+
* and `is_touched` reject a misspelled field at compile time.
|
|
22
|
+
*
|
|
15
23
|
* All trackable inputs must have a `name` attribute — an error is thrown in dev
|
|
16
24
|
* if an input without `name` loses focus.
|
|
17
25
|
*
|
|
18
26
|
* @example
|
|
19
27
|
* ```svelte
|
|
20
|
-
* <script>
|
|
21
|
-
* const form_state = new FormState();
|
|
28
|
+
* <script lang="ts">
|
|
29
|
+
* const form_state = new FormState<'username'>();
|
|
22
30
|
* let username = $state.raw('');
|
|
23
31
|
* const username_valid = $derived(Username.safeParse(username).success);
|
|
24
32
|
* const can_submit = $derived(username.trim() && username_valid);
|
|
@@ -26,6 +26,26 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { AsyncSlot } from './async_slot.svelte.js';
|
|
28
28
|
import type { RoleGrantOfferJson } from '../auth/role_grant_offer_schema.js';
|
|
29
|
+
/**
|
|
30
|
+
* The scope of an offer: global (both absent / `null`) or scoped (both set).
|
|
31
|
+
* `scope_kind` and `scope_id` are paired on the wire — a lone `scope_id` is an
|
|
32
|
+
* `invalid_params` refusal — so the pair is one union rather than two optional
|
|
33
|
+
* fields.
|
|
34
|
+
*/
|
|
35
|
+
export type RoleGrantOfferScope = {
|
|
36
|
+
scope_kind?: null;
|
|
37
|
+
scope_id?: null;
|
|
38
|
+
} | {
|
|
39
|
+
scope_kind: string;
|
|
40
|
+
scope_id: string;
|
|
41
|
+
};
|
|
42
|
+
/** Params for creating an offer — `RoleGrantOffersRpc.create` and `submit_create`. */
|
|
43
|
+
export type RoleGrantOfferCreateParams = {
|
|
44
|
+
to_account_id: string;
|
|
45
|
+
to_actor_id?: string | null;
|
|
46
|
+
role: string;
|
|
47
|
+
message?: string | null;
|
|
48
|
+
} & RoleGrantOfferScope;
|
|
29
49
|
/**
|
|
30
50
|
* Svelte context for `RoleGrantOffersState`.
|
|
31
51
|
* Use `role_grant_offers_state_context.set(state)` in the provider and
|
|
@@ -52,13 +72,7 @@ export interface RoleGrantOffersRpc {
|
|
|
52
72
|
}) => Promise<{
|
|
53
73
|
offers: Array<RoleGrantOfferJson>;
|
|
54
74
|
}>;
|
|
55
|
-
create: (params: {
|
|
56
|
-
to_account_id: string;
|
|
57
|
-
to_actor_id?: string | null;
|
|
58
|
-
role: string;
|
|
59
|
-
scope_id?: string | null;
|
|
60
|
-
message?: string | null;
|
|
61
|
-
}) => Promise<{
|
|
75
|
+
create: (params: RoleGrantOfferCreateParams) => Promise<{
|
|
62
76
|
offer: RoleGrantOfferJson;
|
|
63
77
|
}>;
|
|
64
78
|
accept: (offer_id: string) => Promise<{
|
|
@@ -135,15 +149,11 @@ export declare class RoleGrantOffersState {
|
|
|
135
149
|
*
|
|
136
150
|
* `to_actor_id` (optional) narrows the offer to a specific actor on
|
|
137
151
|
* `to_account_id`; omit / null for the account-grain default (any actor
|
|
138
|
-
* on the recipient account may accept).
|
|
152
|
+
* on the recipient account may accept). A scoped offer passes `scope_kind`
|
|
153
|
+
* and `scope_id` together; an unpaired scope (reachable from untyped
|
|
154
|
+
* callers) fails with `GRANT_SCOPE_PAIRED_MESSAGE` without an RPC.
|
|
139
155
|
*/
|
|
140
|
-
submit_create(params:
|
|
141
|
-
to_account_id: string;
|
|
142
|
-
to_actor_id?: string | null;
|
|
143
|
-
role: string;
|
|
144
|
-
scope_id?: string | null;
|
|
145
|
-
message?: string | null;
|
|
146
|
-
}): Promise<RoleGrantOfferJson | undefined>;
|
|
156
|
+
submit_create(params: RoleGrantOfferCreateParams): Promise<RoleGrantOfferJson | undefined>;
|
|
147
157
|
/** Accept an offer; stamps it terminal in the cache and drops any siblings the server superseded. */
|
|
148
158
|
submit_accept(offer_id: string): Promise<void>;
|
|
149
159
|
submit_decline(offer_id: string, reason?: string | null): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_offers_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/role_grant_offers_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"role_grant_offers_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/role_grant_offers_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAc7E;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAC9B;IAAE,UAAU,CAAC,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnF,sFAAsF;AACtF,MAAM,MAAM,0BAA0B,GAAG;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GAAG,mBAAmB,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,+BAA+B;;;;CAAyC,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAA;KAAE,CAAC,CAAC;IAC3D,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAA;KAAE,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC,CAAC;IACvF,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QACrC,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,kBAAkB,CAAC;QAC1B,oBAAoB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KACpC,CAAC,CAAC;IACH,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7E,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;CACrD;AAED,yFAAyF;AACzF,MAAM,WAAW,0BAA0B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,qFAAqF;AACrF,MAAM,MAAM,uBAAuB,GAAG,CACrC,OAAO,EAAE,CAAC,YAAY,EAAE,0BAA0B,KAAK,IAAI,KACvD,MAAM,IAAI,CAAC;AAEhB,MAAM,WAAW,2BAA2B;IAC3C,GAAG,EAAE,kBAAkB,CAAC;IACxB,oFAAoF;IACpF,UAAU,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC9B;AAQD,qBAAa,oBAAoB;;IAKhC,QAAQ,CAAC,IAAI,0BAAyB;IACtC,QAAQ,CAAC,YAAY,0BAAyB;IAC9C,QAAQ,CAAC,MAAM;;;;;;;;;;;;;;;;;eAAuC;IACtD,QAAQ,CAAC,MAAM,0BAAyB;IACxC,QAAQ,CAAC,OAAO,0BAAyB;IACzC,QAAQ,CAAC,OAAO,0BAAyB;IAIzC,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAezC;IAEH,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAczC;IAEH,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAIxC;IAEH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAkC;gBAErD,OAAO,EAAE,2BAA2B;IAMhD,4DAA4D;IACtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,6DAA6D;IACvD,aAAa,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjF;;;;;;;;OAQG;IACG,aAAa,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAWhG,qGAAqG;IAC/F,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvE,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD;;;OAGG;IACH,SAAS,CAAC,YAAY,EAAE,uBAAuB,GAAG,MAAM,IAAI;IAM5D;;;;;;OAMG;IACH,kBAAkB,CAAC,YAAY,EAAE,0BAA0B,GAAG,IAAI;IAwBlE,4CAA4C;IAC5C,KAAK,IAAI,IAAI;CAwBb"}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { create_context } from '@fuzdev/fuz_ui/context_helpers.ts';
|
|
28
28
|
import { AsyncSlot } from './async_slot.svelte.js';
|
|
29
29
|
import { is_iso8601_seconds_live } from '../timestamp.js';
|
|
30
|
+
import { GRANT_SCOPE_PAIRED_MESSAGE, is_grant_scope_paired } from '../auth/role_grant_offer_action_specs.js';
|
|
30
31
|
import { ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, ROLE_GRANT_REVOKE_NOTIFICATION_METHOD } from '../auth/role_grant_offer_notifications.js';
|
|
31
32
|
/**
|
|
32
33
|
* Svelte context for `RoleGrantOffersState`.
|
|
@@ -121,10 +122,15 @@ export class RoleGrantOffersState {
|
|
|
121
122
|
*
|
|
122
123
|
* `to_actor_id` (optional) narrows the offer to a specific actor on
|
|
123
124
|
* `to_account_id`; omit / null for the account-grain default (any actor
|
|
124
|
-
* on the recipient account may accept).
|
|
125
|
+
* on the recipient account may accept). A scoped offer passes `scope_kind`
|
|
126
|
+
* and `scope_id` together; an unpaired scope (reachable from untyped
|
|
127
|
+
* callers) fails with `GRANT_SCOPE_PAIRED_MESSAGE` without an RPC.
|
|
125
128
|
*/
|
|
126
129
|
async submit_create(params) {
|
|
127
130
|
return this.create.run(async () => {
|
|
131
|
+
if (!is_grant_scope_paired(params)) {
|
|
132
|
+
throw new Error(GRANT_SCOPE_PAIRED_MESSAGE);
|
|
133
|
+
}
|
|
128
134
|
const { offer } = await this.#rpc.create(params);
|
|
129
135
|
this.#merge_offers([offer]);
|
|
130
136
|
return offer;
|