@asturiu/v0-sdk 0.0.0-stage → 1.0.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/CHANGELOG.md +42 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +82 -2
- package/contract/assertion-vectors.json +90 -0
- package/contract/schemas/branding.schema.json +46 -0
- package/contract/schemas/definitions.schema.json +376 -0
- package/contract/schemas/portal.schema.json +56 -0
- package/contract/tenant-api-routes.json +165 -0
- package/dist/branding.d.ts +131 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +206 -0
- package/dist/branding.js.map +1 -0
- package/dist/changesets.d.ts +58 -0
- package/dist/changesets.d.ts.map +1 -0
- package/dist/changesets.js +18 -0
- package/dist/changesets.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +12 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/changesets.d.ts +14 -0
- package/dist/cli/changesets.d.ts.map +1 -0
- package/dist/cli/changesets.js +252 -0
- package/dist/cli/changesets.js.map +1 -0
- package/dist/cli/cli.d.ts +37 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +415 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/context.d.ts +147 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +258 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/engine.d.ts +59 -0
- package/dist/cli/engine.d.ts.map +1 -0
- package/dist/cli/engine.js +85 -0
- package/dist/cli/engine.js.map +1 -0
- package/dist/cli/feedback.d.ts +40 -0
- package/dist/cli/feedback.d.ts.map +1 -0
- package/dist/cli/feedback.js +420 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/files.d.ts +83 -0
- package/dist/cli/files.d.ts.map +1 -0
- package/dist/cli/files.js +389 -0
- package/dist/cli/files.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/overview.d.ts +6 -0
- package/dist/cli/overview.d.ts.map +1 -0
- package/dist/cli/overview.js +379 -0
- package/dist/cli/overview.js.map +1 -0
- package/dist/cli/parts.d.ts +5 -0
- package/dist/cli/parts.d.ts.map +1 -0
- package/dist/cli/parts.js +414 -0
- package/dist/cli/parts.js.map +1 -0
- package/dist/cli/sources.d.ts +37 -0
- package/dist/cli/sources.d.ts.map +1 -0
- package/dist/cli/sources.js +91 -0
- package/dist/cli/sources.js.map +1 -0
- package/dist/cli/terminal.d.ts +135 -0
- package/dist/cli/terminal.d.ts.map +1 -0
- package/dist/cli/terminal.js +353 -0
- package/dist/cli/terminal.js.map +1 -0
- package/dist/cli/ui-page.d.ts +8 -0
- package/dist/cli/ui-page.d.ts.map +1 -0
- package/dist/cli/ui-page.js +216 -0
- package/dist/cli/ui-page.js.map +1 -0
- package/dist/cli/ui.d.ts +54 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +251 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +88 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +165 -0
- package/dist/client.js.map +1 -0
- package/dist/contacts.d.ts +43 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +46 -0
- package/dist/contacts.js.map +1 -0
- package/dist/definition-set.d.ts +731 -0
- package/dist/definition-set.d.ts.map +1 -0
- package/dist/definition-set.js +659 -0
- package/dist/definition-set.js.map +1 -0
- package/dist/definitions.d.ts +42 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +154 -0
- package/dist/definitions.js.map +1 -0
- package/dist/enrolment.d.ts +9 -0
- package/dist/enrolment.d.ts.map +1 -0
- package/dist/enrolment.js +8 -0
- package/dist/enrolment.js.map +1 -0
- package/dist/fill.d.ts +73 -0
- package/dist/fill.d.ts.map +1 -0
- package/dist/fill.js +46 -0
- package/dist/fill.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications.d.ts +33 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +36 -0
- package/dist/notifications.js.map +1 -0
- package/dist/oidc.d.ts +122 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +150 -0
- package/dist/oidc.js.map +1 -0
- package/dist/overlay.d.ts +109 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +58 -0
- package/dist/overlay.js.map +1 -0
- package/dist/passes.d.ts +95 -0
- package/dist/passes.d.ts.map +1 -0
- package/dist/passes.js +140 -0
- package/dist/passes.js.map +1 -0
- package/dist/portal-options.d.ts +65 -0
- package/dist/portal-options.d.ts.map +1 -0
- package/dist/portal-options.js +120 -0
- package/dist/portal-options.js.map +1 -0
- package/dist/reads.d.ts +41 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +44 -0
- package/dist/reads.js.map +1 -0
- package/dist/recovery-words.d.ts +8 -0
- package/dist/recovery-words.d.ts.map +1 -0
- package/dist/recovery-words.js +136 -0
- package/dist/recovery-words.js.map +1 -0
- package/dist/recovery.d.ts +198 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +112 -0
- package/dist/recovery.js.map +1 -0
- package/dist/reveal.d.ts +35 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +66 -0
- package/dist/reveal.js.map +1 -0
- package/dist/seal.d.ts +15 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +0 -0
- package/dist/seal.js.map +1 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +61 -0
- package/dist/session.js.map +1 -0
- package/dist/signing-in-mark.d.ts +14 -0
- package/dist/signing-in-mark.d.ts.map +1 -0
- package/dist/signing-in-mark.js +24 -0
- package/dist/signing-in-mark.js.map +1 -0
- package/dist/signing-in.d.ts +22 -0
- package/dist/signing-in.d.ts.map +1 -0
- package/dist/signing-in.js +203 -0
- package/dist/signing-in.js.map +1 -0
- package/dist/tags.d.ts +96 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +88 -0
- package/dist/tags.js.map +1 -0
- package/dist/tenant-subscription.d.ts +47 -0
- package/dist/tenant-subscription.d.ts.map +1 -0
- package/dist/tenant-subscription.js +7 -0
- package/dist/tenant-subscription.js.map +1 -0
- package/dist/testing/index.d.ts +125 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +320 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/theme.d.ts +237 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +504 -0
- package/dist/theme.js.map +1 -0
- package/dist/tickets.d.ts +152 -0
- package/dist/tickets.d.ts.map +1 -0
- package/dist/tickets.js +65 -0
- package/dist/tickets.js.map +1 -0
- package/dist/tiers.d.ts +168 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +341 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +885 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/urlid.d.ts +50 -0
- package/dist/urlid.d.ts.map +1 -0
- package/dist/urlid.js +78 -0
- package/dist/urlid.js.map +1 -0
- package/dist/version.d.ts +15 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +15 -0
- package/dist/version.js.map +1 -0
- package/make/asturiu.mk +43 -0
- package/package.json +63 -4
- package/src/branding.ts +221 -0
- package/src/changesets.ts +61 -0
- package/src/cli/bin.ts +12 -0
- package/src/cli/changesets.ts +233 -0
- package/src/cli/cli.ts +425 -0
- package/src/cli/context.ts +299 -0
- package/src/cli/engine.ts +112 -0
- package/src/cli/feedback.ts +406 -0
- package/src/cli/files.ts +371 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/overview.ts +343 -0
- package/src/cli/parts.ts +371 -0
- package/src/cli/sources.ts +111 -0
- package/src/cli/terminal.ts +384 -0
- package/src/cli/ui-page.ts +215 -0
- package/src/cli/ui.ts +266 -0
- package/src/client.ts +202 -0
- package/src/contacts.ts +61 -0
- package/src/definition-set.ts +965 -0
- package/src/definitions.ts +156 -0
- package/src/enrolment.ts +10 -0
- package/src/fill.ts +83 -0
- package/src/index.ts +38 -0
- package/src/notifications.ts +55 -0
- package/src/oidc.ts +199 -0
- package/src/overlay.ts +137 -0
- package/src/passes.ts +168 -0
- package/src/portal-options.ts +125 -0
- package/src/reads.ts +71 -0
- package/src/recovery-words.ts +137 -0
- package/src/recovery.ts +269 -0
- package/src/reveal.ts +72 -0
- package/src/seal.ts +0 -0
- package/src/session.ts +93 -0
- package/src/signing-in-mark.ts +23 -0
- package/src/signing-in.ts +213 -0
- package/src/tags.ts +146 -0
- package/src/tenant-subscription.ts +44 -0
- package/src/testing/index.ts +337 -0
- package/src/theme.ts +628 -0
- package/src/tickets.ts +195 -0
- package/src/tiers.ts +397 -0
- package/src/types.ts +847 -0
- package/src/urlid.ts +77 -0
- package/src/version.ts +15 -0
- package/templates/asturiu/branding.json +4 -0
- package/templates/asturiu/definitions.json +23 -0
- package/templates/asturiu/portal.json +16 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { MARK } from "./signing-in-mark.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The "Signing in" screen (components/15, ADR-032): the platform's mark moving
|
|
5
|
+
* in the platform's colours, and "Signing in" — no product name, so it suits
|
|
6
|
+
* every Tenant. The Customer Portal shows it over a flow page that is leaving,
|
|
7
|
+
* and `continuePage` shows it at the Application's callback, so the hand-over
|
|
8
|
+
* from the sign-in host to the Application looks like one screen.
|
|
9
|
+
*
|
|
10
|
+
* Plain markup and CSS, no inline `style` attribute, so a page with a strict
|
|
11
|
+
* Content-Security-Policy only has to allow the one `<style>` element.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One swell of the mark, in milliseconds (`SIGNING_IN_CSS`). */
|
|
15
|
+
const CYCLE_MS = 2800;
|
|
16
|
+
const PAD = 50;
|
|
17
|
+
const ID = "signing-in";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where in its loop the animation starts, as `animation-delay`: the wall
|
|
21
|
+
* clock's position in the cycle. The portal's screen and the callback's page
|
|
22
|
+
* both start there, so the mark does not jump back at the hand-over.
|
|
23
|
+
*/
|
|
24
|
+
export function signingInPhase(now = Date.now()): string {
|
|
25
|
+
return `-${now % CYCLE_MS}ms`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A circle under each drop of the mark, which swells in turn (`signing-in-*`
|
|
30
|
+
* keyframes): the gold head gathers, the side drops reach out, the teal head
|
|
31
|
+
* fills and the teal drop reaches down.
|
|
32
|
+
*/
|
|
33
|
+
const DROPS = [
|
|
34
|
+
{ name: "head", fill: "arch", cx: 514, cy: 110, r: 100 },
|
|
35
|
+
{ name: "drop-left", fill: "arch", cx: 116, cy: 534, r: 100 },
|
|
36
|
+
{ name: "drop-right", fill: "arch", cx: 898, cy: 534, r: 100 },
|
|
37
|
+
{ name: "stem-head", fill: "stem", cx: 512, cy: 420, r: 80 },
|
|
38
|
+
{ name: "stem-drop", fill: "stem", cx: 510, cy: 830, r: 122 },
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The traced mark, drawn as supplied, with the swelling circles. A goo filter
|
|
43
|
+
* (blur, then a hard alpha edge) melts a swollen circle into the outline
|
|
44
|
+
* around it, as a liquid would. `width` and `height` keep it small should the
|
|
45
|
+
* stylesheet be refused.
|
|
46
|
+
*/
|
|
47
|
+
function markSvg(): string {
|
|
48
|
+
const box = [-PAD, -PAD, MARK.width + 2 * PAD, MARK.height + 2 * PAD] as const;
|
|
49
|
+
const drops = (fill: "arch" | "stem") =>
|
|
50
|
+
DROPS.filter((d) => d.fill === fill)
|
|
51
|
+
.map((d) => `<circle class="${ID}-${d.name}" cx="${d.cx}" cy="${d.cy}" r="${d.r}"/>`)
|
|
52
|
+
.join("");
|
|
53
|
+
return (
|
|
54
|
+
`<svg class="${ID}-mark" viewBox="${box.join(" ")}" width="156" height="${Math.round((156 * box[3]) / box[2])}" aria-hidden="true" focusable="false">` +
|
|
55
|
+
`<defs>` +
|
|
56
|
+
`<linearGradient id="${ID}-arch" gradientUnits="userSpaceOnUse" x1="140" y1="520" x2="420" y2="230"><stop offset="0.3" class="${ID}-teal"/><stop offset="0.78" class="${ID}-gold"/></linearGradient>` +
|
|
57
|
+
`<linearGradient id="${ID}-stem" gradientUnits="userSpaceOnUse" x1="0" y1="440" x2="0" y2="970"><stop offset="0.2" class="${ID}-teal"/><stop offset="1" class="${ID}-teal-deep"/></linearGradient>` +
|
|
58
|
+
`<filter id="${ID}-goo" filterUnits="userSpaceOnUse" x="${box[0]}" y="${box[1]}" width="${box[2]}" height="${box[3]}" color-interpolation-filters="sRGB">` +
|
|
59
|
+
`<feGaussianBlur in="SourceGraphic" stdDeviation="16"/><feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9"/><feComposite in="SourceGraphic" operator="over"/>` +
|
|
60
|
+
`</filter>` +
|
|
61
|
+
`</defs>` +
|
|
62
|
+
`<g filter="url(#${ID}-goo)">` +
|
|
63
|
+
`<g fill="url(#${ID}-arch)"><path d="${MARK.arch}"/>${drops("arch")}</g>` +
|
|
64
|
+
`<g fill="url(#${ID}-stem)"><path d="${MARK.stem}"/>${drops("stem")}</g>` +
|
|
65
|
+
`</g>` +
|
|
66
|
+
`<g class="${ID}-gloss">${MARK.gloss.map((d) => `<path d="${d}"/>`).join("")}</g>` +
|
|
67
|
+
`</svg>`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What goes inside the screen's `.signing-in` element: the mark and the label.
|
|
75
|
+
* With `href` the label is a link there, for a browser that does not follow a
|
|
76
|
+
* `<meta refresh>`.
|
|
77
|
+
*/
|
|
78
|
+
export function signingInMarkup(opts: { text?: string; href?: string } = {}): string {
|
|
79
|
+
const text = esc(opts.text ?? "Signing in");
|
|
80
|
+
const label = opts.href ? `<a href="${esc(opts.href)}">${text}</a>` : text;
|
|
81
|
+
return `${markSvg()}<p class="${ID}-text">${label}</p>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The screen's stylesheet. `.signing-in` is a fixed cover over whatever page
|
|
86
|
+
* it sits on; `.signing-in.fade` fades it in. Light or dark follows the
|
|
87
|
+
* system, unless the page's root says `data-theme="light"`.
|
|
88
|
+
*/
|
|
89
|
+
export const SIGNING_IN_CSS = `
|
|
90
|
+
.signing-in {
|
|
91
|
+
--si-gold: #f2c230;
|
|
92
|
+
--si-teal: #35a8a4;
|
|
93
|
+
--si-teal-deep: #2b7f8b;
|
|
94
|
+
--si-ground: #f4f4f1;
|
|
95
|
+
--si-glow: 14%;
|
|
96
|
+
--si-text: color-mix(in srgb, #0a0b0d 72%, var(--si-ground));
|
|
97
|
+
position: fixed;
|
|
98
|
+
inset: 0;
|
|
99
|
+
z-index: 100;
|
|
100
|
+
display: grid;
|
|
101
|
+
place-content: center;
|
|
102
|
+
justify-items: center;
|
|
103
|
+
gap: 28px;
|
|
104
|
+
padding: env(safe-area-inset-top, 0px) 24px env(safe-area-inset-bottom, 0px);
|
|
105
|
+
background:
|
|
106
|
+
radial-gradient(260px 230px at 50% calc(50% - 20px), color-mix(in srgb, var(--si-gold) var(--si-glow), transparent), transparent),
|
|
107
|
+
var(--si-ground);
|
|
108
|
+
color-scheme: light;
|
|
109
|
+
}
|
|
110
|
+
@media (prefers-color-scheme: dark) {
|
|
111
|
+
:root:not([data-theme="light"]) .signing-in {
|
|
112
|
+
--si-ground: #0a0b0d;
|
|
113
|
+
--si-glow: 7%;
|
|
114
|
+
--si-text: #a5aab3;
|
|
115
|
+
color-scheme: dark;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
.signing-in.fade {
|
|
119
|
+
animation: signing-in-fade 0.2s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
120
|
+
}
|
|
121
|
+
@keyframes signing-in-fade {
|
|
122
|
+
from {
|
|
123
|
+
opacity: 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
.signing-in-mark {
|
|
127
|
+
width: 156px;
|
|
128
|
+
height: auto;
|
|
129
|
+
overflow: visible;
|
|
130
|
+
}
|
|
131
|
+
@media (min-width: 720px) {
|
|
132
|
+
.signing-in-mark {
|
|
133
|
+
width: 176px;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
.signing-in-gold {
|
|
137
|
+
stop-color: var(--si-gold);
|
|
138
|
+
}
|
|
139
|
+
.signing-in-teal {
|
|
140
|
+
stop-color: var(--si-teal);
|
|
141
|
+
}
|
|
142
|
+
.signing-in-teal-deep {
|
|
143
|
+
stop-color: var(--si-teal-deep);
|
|
144
|
+
}
|
|
145
|
+
.signing-in-gloss {
|
|
146
|
+
fill: #fff;
|
|
147
|
+
opacity: 0.5;
|
|
148
|
+
}
|
|
149
|
+
/*
|
|
150
|
+
* One swell runs through the mark every 2.8 s: each circle grows past its drop
|
|
151
|
+
* (the goo filter melts it in), overshoots back a little and rests. CSS, not
|
|
152
|
+
* SMIL: Chromium starts SMIL at the load event, CSS from the first paint.
|
|
153
|
+
*/
|
|
154
|
+
.signing-in-mark circle {
|
|
155
|
+
transform-box: fill-box;
|
|
156
|
+
transform-origin: center;
|
|
157
|
+
/* Longhands: a shorthand here would reset the name each drop sets below. */
|
|
158
|
+
animation-duration: ${CYCLE_MS}ms;
|
|
159
|
+
animation-delay: var(--si-phase, 0s);
|
|
160
|
+
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
|
161
|
+
animation-iteration-count: infinite;
|
|
162
|
+
}
|
|
163
|
+
.signing-in-head { animation-name: signing-in-head; }
|
|
164
|
+
.signing-in-drop-left { animation-name: signing-in-drop-left; }
|
|
165
|
+
.signing-in-drop-right { animation-name: signing-in-drop-right; }
|
|
166
|
+
.signing-in-stem-head { animation-name: signing-in-stem-head; }
|
|
167
|
+
.signing-in-stem-drop { animation-name: signing-in-stem-drop; }
|
|
168
|
+
@keyframes signing-in-head {
|
|
169
|
+
0%, 2%, 36%, 100% { transform: none; }
|
|
170
|
+
13% { transform: translate(0, -18px) scale(1.18); }
|
|
171
|
+
27% { transform: translate(0, 4px) scale(0.98); }
|
|
172
|
+
}
|
|
173
|
+
@keyframes signing-in-drop-left {
|
|
174
|
+
0%, 16%, 55%, 100% { transform: none; }
|
|
175
|
+
32% { transform: translate(-34px, 28px) scale(1.08); }
|
|
176
|
+
46% { transform: translate(6px, -5px) scale(0.99); }
|
|
177
|
+
}
|
|
178
|
+
@keyframes signing-in-drop-right {
|
|
179
|
+
0%, 16%, 55%, 100% { transform: none; }
|
|
180
|
+
32% { transform: translate(34px, 28px) scale(1.08); }
|
|
181
|
+
46% { transform: translate(-6px, -5px) scale(0.99); }
|
|
182
|
+
}
|
|
183
|
+
@keyframes signing-in-stem-head {
|
|
184
|
+
0%, 34%, 66%, 100% { transform: none; }
|
|
185
|
+
46% { transform: translate(0, -6px) scale(1.25); }
|
|
186
|
+
58% { transform: translate(0, 2px) scale(0.975); }
|
|
187
|
+
}
|
|
188
|
+
@keyframes signing-in-stem-drop {
|
|
189
|
+
0%, 46%, 86%, 100% { transform: none; }
|
|
190
|
+
62% { transform: translate(0, 38px) scale(1.066); }
|
|
191
|
+
76% { transform: translate(0, -6px) scale(0.992); }
|
|
192
|
+
}
|
|
193
|
+
/* Held at rest: shortening the durations, as reduced-motion rules often do, would make the loop flicker. */
|
|
194
|
+
@media (prefers-reduced-motion: reduce) {
|
|
195
|
+
.signing-in-mark circle {
|
|
196
|
+
animation: none !important;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
.signing-in-text {
|
|
200
|
+
margin: 0;
|
|
201
|
+
/* Letter spacing trails the last letter too: pad the start to keep the word centred. */
|
|
202
|
+
padding-left: 0.28em;
|
|
203
|
+
/* The portal's own stack, so the screen is the same on the portal and at a callback. */
|
|
204
|
+
font: 600 12px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
205
|
+
letter-spacing: 0.28em;
|
|
206
|
+
text-transform: uppercase;
|
|
207
|
+
color: var(--si-text);
|
|
208
|
+
}
|
|
209
|
+
.signing-in-text a {
|
|
210
|
+
color: inherit;
|
|
211
|
+
text-decoration: none;
|
|
212
|
+
}
|
|
213
|
+
`;
|
package/src/tags.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tags (ADR-057). A tag marks a subset of Users or Tenants (the platform's
|
|
3
|
+
* scope) or of one Tenant's Customers (that Tenant's scope). It is derived —
|
|
4
|
+
* computed from what the Engine already records, never stored — or assigned
|
|
5
|
+
* by a person or by a rule from a closed list. A tag selects; it never grants.
|
|
6
|
+
*
|
|
7
|
+
* Pure and dependency-free: the Operations API stores and evaluates, the Admin
|
|
8
|
+
* UI and the Tenant's tools read the same vocabulary.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const TAG_SUBJECTS = ["user", "customer", "tenant"] as const;
|
|
13
|
+
|
|
14
|
+
export type TagSubject = (typeof TAG_SUBJECTS)[number];
|
|
15
|
+
|
|
16
|
+
/** Who sees a tag on a subject (§4): staff only, also the subject itself, or (a platform tag on a Tenant) also the Tenant's staff. */
|
|
17
|
+
export const TAG_VISIBILITIES = ["staff", "subject", "tenant"] as const;
|
|
18
|
+
|
|
19
|
+
export type TagVisibility = (typeof TAG_VISIBILITIES)[number];
|
|
20
|
+
|
|
21
|
+
/** The Admin UI's tag colours: a tag is told apart at a glance, never by colour alone. */
|
|
22
|
+
export const TAG_COLOURS = ["slate", "gold", "teal", "green", "amber", "red", "violet", "blue"] as const;
|
|
23
|
+
|
|
24
|
+
export type TagColour = (typeof TAG_COLOURS)[number];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The closed list of rules (§3). A rule assigns when its moment comes and never removes. `on_hard_bounce` and
|
|
28
|
+
* `on_complaint` (ADR-060 §5): mail to the subject's address bounced for good, or was reported as spam.
|
|
29
|
+
*/
|
|
30
|
+
export type TagRule = { kind: "at_creation" } | { kind: "on_join" } | { kind: "on_tier"; tier: string } | { kind: "on_restore" } | { kind: "on_hard_bounce" } | { kind: "on_complaint" };
|
|
31
|
+
|
|
32
|
+
export const TAG_RULE_KINDS = ["at_creation", "on_join", "on_tier", "on_restore", "on_hard_bounce", "on_complaint"] as const;
|
|
33
|
+
|
|
34
|
+
export type TagRuleKind = (typeof TAG_RULE_KINDS)[number];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What carrying a tag does (ADR-060 §4), from a closed list, beside the rules that assign it. An effect never grants:
|
|
38
|
+
* `no_email` only takes e-mail away. Mail to a subject with such a tag is not sent; in-app Notifications and
|
|
39
|
+
* tickets still appear. A Customer's mail also stops while its User carries one.
|
|
40
|
+
*/
|
|
41
|
+
export const TAG_EFFECTS = ["no_email"] as const;
|
|
42
|
+
|
|
43
|
+
export type TagEffect = (typeof TAG_EFFECTS)[number];
|
|
44
|
+
|
|
45
|
+
/** An assigned tag's name (§2): lowercase letters, digits and `-`, 2 to 40 characters, never `:` or `=`. */
|
|
46
|
+
export const TAG_NAME = /^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$/;
|
|
47
|
+
|
|
48
|
+
export function tagNameProblem(name: string): string | null {
|
|
49
|
+
if (TAG_NAME.test(name)) return null;
|
|
50
|
+
return "2 to 40 lowercase letters, digits or -, starting and ending with a letter or digit";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A derived tag is written `<kind>=<value>`; an assigned one never contains `=`. */
|
|
54
|
+
export function isDerivedTag(name: string): boolean {
|
|
55
|
+
return name.includes("=");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A tag as answers carry it on a subject. */
|
|
59
|
+
export interface TagRef {
|
|
60
|
+
/** The tag's id; absent for a derived tag, which is no record. */
|
|
61
|
+
id?: string;
|
|
62
|
+
name: string;
|
|
63
|
+
derived: boolean;
|
|
64
|
+
colour?: TagColour;
|
|
65
|
+
visibility?: TagVisibility;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A tag's record, as the Tags page and the Tenant API read it. */
|
|
69
|
+
export interface TagView {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
/** `<slug>:<name>` for a Tenant tag, the bare name for a platform one. */
|
|
73
|
+
shown: string;
|
|
74
|
+
scope: "platform" | "tenant";
|
|
75
|
+
/** The Tenant's ARID, for a Tenant tag. */
|
|
76
|
+
tenant?: string;
|
|
77
|
+
subject: TagSubject;
|
|
78
|
+
visibility: TagVisibility;
|
|
79
|
+
description: string;
|
|
80
|
+
colour: TagColour;
|
|
81
|
+
rules: TagRule[];
|
|
82
|
+
/** What carrying it does (ADR-060 §4). */
|
|
83
|
+
effects: TagEffect[];
|
|
84
|
+
/** Kept by the Engine in every scope: cannot be deleted or renamed, its effects and rules are fixed. */
|
|
85
|
+
system: boolean;
|
|
86
|
+
subjects: number;
|
|
87
|
+
created_at: string;
|
|
88
|
+
updated_at: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** How a tag reads on a page: `<slug>:<name>` in a Tenant's scope (§1). */
|
|
92
|
+
export function shownTagName(name: string, slug?: string | null): string {
|
|
93
|
+
return slug ? `${slug}:${name}` : name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A selector (§6): every tag of `all`, at least one of `any`, none of `none`. An empty selector selects everyone. */
|
|
97
|
+
export interface TagSelector {
|
|
98
|
+
all?: string[];
|
|
99
|
+
any?: string[];
|
|
100
|
+
none?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function selectorMatches(selector: TagSelector, tags: Iterable<string>): boolean {
|
|
104
|
+
const has = new Set(tags);
|
|
105
|
+
if (selector.all?.some((t) => !has.has(t))) return false;
|
|
106
|
+
if (selector.any?.length && !selector.any.some((t) => has.has(t))) return false;
|
|
107
|
+
if (selector.none?.some((t) => has.has(t))) return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function selectorTags(selector: TagSelector): string[] {
|
|
112
|
+
return Array.from(new Set([...(selector.all ?? []), ...(selector.any ?? []), ...(selector.none ?? [])]));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Why a selector cannot be used, or null: each entry an assigned name or a derived `<kind>=<value>`, at most 20 in all. */
|
|
116
|
+
export function selectorProblem(selector: unknown): string | null {
|
|
117
|
+
if (!selector || typeof selector !== "object" || Array.isArray(selector)) return "a selector is { all?, any?, none? }";
|
|
118
|
+
const s = selector as Record<string, unknown>;
|
|
119
|
+
for (const k of Object.keys(s)) if (k !== "all" && k !== "any" && k !== "none") return `unknown key ${k}`;
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (const k of ["all", "any", "none"] as const) {
|
|
122
|
+
const v = s[k];
|
|
123
|
+
if (v === undefined) continue;
|
|
124
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) return `${k} is a list of tag names`;
|
|
125
|
+
for (const t of v as string[]) {
|
|
126
|
+
n++;
|
|
127
|
+
if (isDerivedTag(t) ? !/^[a-z_]+=[a-z0-9_.-]+$/.test(t) : !TAG_NAME.test(t)) return `${t} is not a tag name`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return n > 20 ? "at most 20 tags in a selector" : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A selector as a query parameter (`?tags=`): JSON, so a list filter reads it back whole. */
|
|
134
|
+
export function encodeSelector(selector: TagSelector): string {
|
|
135
|
+
return JSON.stringify(selector);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function decodeSelector(raw: string | null | undefined): TagSelector | null {
|
|
139
|
+
if (!raw) return null;
|
|
140
|
+
try {
|
|
141
|
+
const s = JSON.parse(raw) as unknown;
|
|
142
|
+
return selectorProblem(s) ? null : (s as TagSelector);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant's own subscription to the platform (ADR-064 §11), as
|
|
3
|
+
* `GET /tenant/v0/subscription` answers it: read-only, for the Tenant's server
|
|
4
|
+
* or its agent. The catalogue and the rules stay with the Engine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TenantPlatformFeatures {
|
|
8
|
+
own_signin: boolean;
|
|
9
|
+
own_domain: boolean;
|
|
10
|
+
/** How many people may hold a role in this Tenant. */
|
|
11
|
+
team_members: number;
|
|
12
|
+
/** A response time the Admin UI states, and nothing enforces. */
|
|
13
|
+
support: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `Tenant.overage`: off unless a Tenant opts in. `cap` limits what it adds to one month's invoice. */
|
|
17
|
+
export interface TenantOverage {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
cap?: { amount: string; currency: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What `GET /tenant/v0/subscription` answers: the Tenant's own tier and use, read-only. */
|
|
23
|
+
export interface TenantSubscriptionView {
|
|
24
|
+
/** The `sbs`, for staff and for a Tenant's own routes. */
|
|
25
|
+
arid: string | null;
|
|
26
|
+
tier: string | null;
|
|
27
|
+
label: string | null;
|
|
28
|
+
state: string | null;
|
|
29
|
+
frozen: boolean;
|
|
30
|
+
renews: boolean | null;
|
|
31
|
+
period_end: string | null;
|
|
32
|
+
trial_end: string | null;
|
|
33
|
+
allowance: TenantAllowance | null;
|
|
34
|
+
used: { active_customers: number; storage: number };
|
|
35
|
+
overage: TenantOverage | null;
|
|
36
|
+
closing_at: string | null;
|
|
37
|
+
features: TenantPlatformFeatures | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What a Tenant's tier includes before overage (ADR-064 §5). */
|
|
41
|
+
export interface TenantAllowance {
|
|
42
|
+
active_customers: number;
|
|
43
|
+
storage: number;
|
|
44
|
+
}
|