@nauticana/sail 1.1.34 → 1.1.35
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 +22 -15
- package/package.json +1 -1
- package/src/agent/component/approval_inbox.html +49 -2
- package/src/agent/component/approval_inbox.ts +54 -16
- package/src/agent/component/assistant_activity.html +28 -0
- package/src/agent/component/assistant_activity.ts +40 -0
- package/src/agent/component/studio_page.html +35 -1
- package/src/agent/component/studio_page.ts +67 -10
- package/src/agent/component/turn_thread.html +27 -0
- package/src/agent/component/turn_thread.ts +83 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/model/agent_studio.ts +63 -0
- package/src/agent/model/approval.ts +37 -1
- package/src/agent/model/conversation.ts +31 -0
- package/src/agent/model/decision.ts +12 -8
- package/src/agent/service/agent_studio.service.ts +29 -0
- package/src/agent/service/approval.service.ts +24 -4
- package/src/agent/service/conversation.service.ts +10 -0
- package/src/component/agency/agency_earnings_summary.html +3 -3
- package/src/component/agency/agency_earnings_summary.ts +2 -2
- package/src/component/agency/agency_payout_history.html +1 -1
- package/src/component/agency/agency_payout_history.ts +2 -3
- package/src/component/agency/commission_ledger.html +1 -1
- package/src/component/agency/commission_ledger.ts +2 -3
- package/src/component/billing/plan_selector.ts +1 -1
- package/src/component/billing/price_selector.ts +5 -5
- package/src/component/payout/bank_info_form.html +15 -6
- package/src/component/payout/bank_info_form.ts +28 -72
- package/src/component/security/api_keys.html +78 -0
- package/src/component/security/api_keys.ts +90 -0
- package/src/component/security/connected_apps.html +25 -0
- package/src/component/security/connected_apps.ts +40 -0
- package/src/index.ts +7 -0
- package/src/model/access.ts +28 -0
- package/src/model/appdata.ts +41 -67
- package/src/model/geo.ts +12 -0
- package/src/service/access.service.ts +57 -0
- package/src/service/caption_pipe.ts +8 -2
- package/src/service/currency.service.ts +51 -0
- package/src/service/money_pipe.ts +27 -0
- package/src/service/rest_service.ts +1 -1
- package/src/service/rest_url.ts +3 -0
- package/src/util/money.ts +18 -34
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Observable, catchError, throwError } from 'rxjs';
|
|
3
|
+
import { ApiKey, MintedApiKey, OAuthGrant } from '../model/access';
|
|
4
|
+
import { TableAction } from '../model/appdata';
|
|
5
|
+
import { BackendService } from './rest_service';
|
|
6
|
+
import { RestURL } from './rest_url';
|
|
7
|
+
|
|
8
|
+
/** The signed-in user's API keys (keel `api_key` table and actions) and OAuth grants. */
|
|
9
|
+
@Injectable({ providedIn: 'root' })
|
|
10
|
+
export class AccessService extends BackendService {
|
|
11
|
+
apiKeys(): Observable<ApiKey[]> {
|
|
12
|
+
return this.list<ApiKey>('api_key', undefined, { orderBy: 'created_at desc' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
mintApiKey(keyName: string, scopes: string[], allowedCidrs: string): Observable<MintedApiKey> {
|
|
16
|
+
return this.apiKeyAction<MintedApiKey>('generate',
|
|
17
|
+
{ key_name: keyName, scopes: scopes.join(','), allowed_cidrs: allowedCidrs });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
restrictApiKey(id: number, allowedCidrs: string): Observable<void> {
|
|
21
|
+
return this.apiKeyAction<void>('restrict', { id, allowed_cidrs: allowedCidrs });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
revokeApiKey(id: number): Observable<{ message: string }> {
|
|
25
|
+
return this.delete('api_key', { Id: String(id) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
grants(): Observable<OAuthGrant[]> {
|
|
29
|
+
return this.http.get<OAuthGrant[]>(this.url(RestURL.oauthGrantsURL))
|
|
30
|
+
.pipe(catchError((err) => this.handleError(err)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
revokeGrant(clientId: string): Observable<void> {
|
|
34
|
+
return this.http.post<void>(this.url(RestURL.oauthGrantRevokeURL), { clientId })
|
|
35
|
+
.pipe(catchError((err) => this.handleError(err)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
canApiKeyAction(name: string): boolean {
|
|
39
|
+
const action = this.findApiKeyAction(name);
|
|
40
|
+
return !!action && this.auth.canExecute(action.authorityObject, action.authorityCheck, 'api_key');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
canRevokeApiKey(): boolean {
|
|
44
|
+
return this.auth.canDelete('api_key');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private apiKeyAction<T>(name: string, body: Record<string, unknown>): Observable<T> {
|
|
48
|
+
const action = this.findApiKeyAction(name);
|
|
49
|
+
return action
|
|
50
|
+
? this.executeAction<T>(action.method, body)
|
|
51
|
+
: throwError(() => new Error(`sail: api_key/${name} is not available in backend metadata.`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private findApiKeyAction(name: string): TableAction | undefined {
|
|
55
|
+
return (this.auth.getTableDefinition('api_key')?.Actions ?? []).find((action) => action.action === name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -18,8 +18,14 @@ export class CaptionPipe implements PipeTransform {
|
|
|
18
18
|
this.loaded();
|
|
19
19
|
if (!this.requested.has(constantId)) {
|
|
20
20
|
this.requested.add(constantId);
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A cached domain emits synchronously: its caption is already current, and a signal
|
|
22
|
+
// write during render would throw NG0600.
|
|
23
|
+
let rendering = true;
|
|
24
|
+
this.labels.loadLabels(constantId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
25
|
+
next: () => { if (!rendering) this.loaded.update((n) => n + 1); },
|
|
26
|
+
error: () => this.requested.delete(constantId),
|
|
27
|
+
});
|
|
28
|
+
rendering = false;
|
|
23
29
|
}
|
|
24
30
|
return this.labels.getLabel(constantId, code);
|
|
25
31
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Injectable, inject, signal } from '@angular/core';
|
|
2
|
+
import { Observable, map, of, shareReplay, tap } from 'rxjs';
|
|
3
|
+
import { Currency } from '../model/geo';
|
|
4
|
+
import { formatMinor } from '../util/money';
|
|
5
|
+
import { BackendService } from './rest_service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* keel's `currency` catalog, the only source of minor-unit exponents. Loaded once through
|
|
9
|
+
* generic REST, so the caller's role needs `TABLE SELECT currency`.
|
|
10
|
+
*/
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class CurrencyService {
|
|
13
|
+
private readonly backend = inject(BackendService);
|
|
14
|
+
private readonly byCode = new Map<string, Currency>();
|
|
15
|
+
private pending?: Observable<void>;
|
|
16
|
+
readonly ready = signal(false);
|
|
17
|
+
readonly failed = signal(false);
|
|
18
|
+
|
|
19
|
+
/** Load the catalog; completed and concurrent calls share one request, a failed one is retried. */
|
|
20
|
+
load(): Observable<void> {
|
|
21
|
+
if (this.ready()) return of(undefined);
|
|
22
|
+
this.failed.set(false);
|
|
23
|
+
this.pending ??= this.backend.list<Currency>('currency', undefined, { limit: 1000 }).pipe(
|
|
24
|
+
tap({
|
|
25
|
+
next: (rows) => {
|
|
26
|
+
for (const row of rows) this.byCode.set(row.Code, row);
|
|
27
|
+
this.ready.set(true);
|
|
28
|
+
},
|
|
29
|
+
error: () => {
|
|
30
|
+
this.pending = undefined;
|
|
31
|
+
this.failed.set(true);
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
map(() => undefined),
|
|
35
|
+
shareReplay({ bufferSize: 1, refCount: false }),
|
|
36
|
+
);
|
|
37
|
+
return this.pending;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Minor-unit exponent of a code; an unknown code is an error, never a guess. */
|
|
41
|
+
exponent(currency: string): number {
|
|
42
|
+
if (!this.ready()) throw new Error('sail: the currency catalog is not loaded.');
|
|
43
|
+
const row = this.byCode.get(currency);
|
|
44
|
+
if (!row) throw new Error(`sail: unknown currency "${currency}".`);
|
|
45
|
+
return row.Exponent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
format(minor: number, currency: string): string {
|
|
49
|
+
return formatMinor(minor, currency, this.exponent(currency));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DestroyRef, Pipe, PipeTransform, inject } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { formatMinor } from '../util/money';
|
|
4
|
+
import { CurrencyService } from './currency.service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `{{ minor | sailMoney:currency }}` formats integer minor units at the catalog exponent, '' until
|
|
8
|
+
* the catalog loads; `{{ minor | sailMoney:currency:exponent }}` uses the exponent keel sent with
|
|
9
|
+
* the amount and needs no catalog. Impure so it re-renders once the catalog is ready.
|
|
10
|
+
*/
|
|
11
|
+
@Pipe({ name: 'sailMoney', pure: false })
|
|
12
|
+
export class MoneyPipe implements PipeTransform {
|
|
13
|
+
private readonly currencies = inject(CurrencyService);
|
|
14
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
15
|
+
private requested = false;
|
|
16
|
+
|
|
17
|
+
transform(minor: number, currency: string, exponent?: number): string {
|
|
18
|
+
if (exponent !== undefined) return formatMinor(minor, currency, exponent);
|
|
19
|
+
if (this.currencies.ready()) return this.currencies.format(minor, currency);
|
|
20
|
+
if (this.currencies.failed()) throw new Error('sail: failed to load the currency catalog.');
|
|
21
|
+
if (!this.requested) {
|
|
22
|
+
this.requested = true;
|
|
23
|
+
this.currencies.load().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ error: () => undefined });
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -39,7 +39,7 @@ export class SailApiError extends Error {
|
|
|
39
39
|
|
|
40
40
|
@Injectable({providedIn: 'root'})
|
|
41
41
|
export class BackendService extends BaseRestService {
|
|
42
|
-
|
|
42
|
+
protected readonly auth = inject(BaseAuthService);
|
|
43
43
|
|
|
44
44
|
/** Resolved per call so configureRestUrls() may run any time before the first request. */
|
|
45
45
|
protected get prefix(): string {
|
package/src/service/rest_url.ts
CHANGED
|
@@ -25,6 +25,9 @@ export const RestURL = {
|
|
|
25
25
|
trustedDeviceRevokeURL: '/api/user/trusted-device/revoke',
|
|
26
26
|
sessionListURL: '/api/user/sessions',
|
|
27
27
|
sessionRevokeURL: '/api/user/sessions/revoke',
|
|
28
|
+
// keel GrantHandler (v1.2.112): the signed-in user's OAuth grants.
|
|
29
|
+
oauthGrantsURL: '/api/user/oauth-grants',
|
|
30
|
+
oauthGrantRevokeURL: '/api/user/oauth-grants/revoke',
|
|
28
31
|
checkoutURL: '/api/billing/checkout',
|
|
29
32
|
subscriptionURL: '/api/billing/subscription',
|
|
30
33
|
cancelSubURL: '/api/billing/subscription/cancel',
|
package/src/util/money.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import { PlanPrice } from '../model/appdata';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Format
|
|
5
|
-
*
|
|
6
|
-
* for an unknown currency code rather than throwing.
|
|
4
|
+
* Format integer minor units at the currency's exponent from keel's `currency` table.
|
|
5
|
+
* `Intl` supplies only the symbol and grouping; it never decides the decimals.
|
|
7
6
|
*/
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return `${amount} ${currency}`;
|
|
7
|
+
export function formatMinor(minor: number, currency: string, exponent: number): string {
|
|
8
|
+
if (!Number.isSafeInteger(minor)) throw new Error(`sail: money must be a safe integer (${minor}).`);
|
|
9
|
+
if (!Number.isInteger(exponent) || exponent < 0 || exponent > 6) {
|
|
10
|
+
throw new Error(`sail: currency "${currency}" has no valid exponent (${exponent}).`);
|
|
13
11
|
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Format an integer minor-unit amount using the currency's exponent reported
|
|
18
|
-
* by Intl (JPY 0, USD 2, BHD 3, ...). No hardcoded /100 assumption.
|
|
19
|
-
*/
|
|
20
|
-
export function formatMinorCurrency(minor: number, currency: string): string {
|
|
21
12
|
const code = currency.trim().toUpperCase();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} catch {
|
|
27
|
-
return `${minor} ${code}`;
|
|
28
|
-
}
|
|
13
|
+
if (!code) throw new Error('sail: currency is required.');
|
|
14
|
+
return new Intl.NumberFormat(undefined, {
|
|
15
|
+
style: 'currency', currency: code, minimumFractionDigits: exponent, maximumFractionDigits: exponent,
|
|
16
|
+
}).format(minor / 10 ** exponent);
|
|
29
17
|
}
|
|
30
18
|
|
|
31
19
|
/** PERIOD_TYPE code vocabulary — the single source for cycle labels/durations. */
|
|
@@ -38,19 +26,15 @@ export const PERIOD_TYPE_INFO: Record<string, { label: string; noun: string; suf
|
|
|
38
26
|
};
|
|
39
27
|
|
|
40
28
|
/**
|
|
41
|
-
* "From" price teaser for a plan card
|
|
42
|
-
*
|
|
43
|
-
* compared within one currency, rendered with its cycle ("from $30.00/mo").
|
|
44
|
-
* Returns '' when the plan has no priced offers (free/trial). The exact offer
|
|
45
|
-
* is chosen later via `<sail-price-selector>`.
|
|
29
|
+
* "From" price teaser for a plan card: the cheapest offer per month within the first
|
|
30
|
+
* offer's currency, rendered with its cycle ("from $30.00/mo"); '' when nothing is priced.
|
|
46
31
|
*/
|
|
47
|
-
export function fromPriceLabel(prices: PlanPrice[] | undefined): string {
|
|
48
|
-
const priced = (prices ?? []).filter((p) => p.
|
|
32
|
+
export function fromPriceLabel(prices: readonly PlanPrice[] | null | undefined): string {
|
|
33
|
+
const priced = (prices ?? []).filter((p) => p.amountMinor > 0);
|
|
49
34
|
if (priced.length === 0) return '';
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const perMonth = (p: PlanPrice) => p.amount / (PERIOD_TYPE_INFO[p.billingCycle]?.months ?? 1);
|
|
35
|
+
const comparable = priced.filter((p) => p.currency === priced[0].currency);
|
|
36
|
+
const perMonth = (p: PlanPrice) => p.amountMinor / (PERIOD_TYPE_INFO[p.billingCycle]?.months ?? 1);
|
|
53
37
|
const lowest = comparable.reduce((a, b) => (perMonth(b) < perMonth(a) ? b : a));
|
|
54
|
-
const formatted =
|
|
55
|
-
return
|
|
38
|
+
const formatted = formatMinor(lowest.amountMinor, lowest.currency, lowest.exponent) + (PERIOD_TYPE_INFO[lowest.billingCycle]?.suffix ?? '');
|
|
39
|
+
return comparable.length > 1 ? `from ${formatted}` : formatted;
|
|
56
40
|
}
|