auggy 0.3.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 +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage record shapes + the abstract VisitorAuthStore interface.
|
|
3
|
+
* Splitting the storage contract from the SQLite impl lets us swap to
|
|
4
|
+
* a Postgres-backed store later without touching the augment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type TokenStatus = "open" | "consumed" | "expired" | "unknown";
|
|
8
|
+
|
|
9
|
+
export interface IssueTokenArgs {
|
|
10
|
+
token: string;
|
|
11
|
+
email: string;
|
|
12
|
+
peerId: string;
|
|
13
|
+
threadId: string;
|
|
14
|
+
expiresAt: number; // epoch ms
|
|
15
|
+
sourceMessageId: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ConsumeTokenResult {
|
|
19
|
+
/** True iff exactly one row transitioned from consumed=0 to consumed=1. */
|
|
20
|
+
consumed: boolean;
|
|
21
|
+
/** Set when consumed=true. */
|
|
22
|
+
email?: string;
|
|
23
|
+
/** Set when consumed=true. */
|
|
24
|
+
peerId?: string;
|
|
25
|
+
/** Set when consumed=true. */
|
|
26
|
+
threadId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VerifiedVisitorRow {
|
|
30
|
+
visitorId: string; // vis_<uuid>
|
|
31
|
+
email: string;
|
|
32
|
+
verifiedAt: number; // epoch ms
|
|
33
|
+
lastSeenAt: number | null;
|
|
34
|
+
reverifyDueAt: number; // epoch ms
|
|
35
|
+
revoked: boolean;
|
|
36
|
+
revokedAt: number | null;
|
|
37
|
+
revokedReason: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface OpenTokenForPeer {
|
|
41
|
+
token: string;
|
|
42
|
+
email: string;
|
|
43
|
+
expiresAt: number; // epoch ms
|
|
44
|
+
issuedAt: number; // epoch ms
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface VisitorAuthStore {
|
|
48
|
+
/**
|
|
49
|
+
* Idempotent schema apply. Safe to call repeatedly; safe on a fresh DB.
|
|
50
|
+
* Called from onBoot before any other operation.
|
|
51
|
+
*/
|
|
52
|
+
initialize(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Insert a new token row. Throws on PK collision (caller should generate
|
|
55
|
+
* a fresh UUID — collisions are statistically impossible in normal use).
|
|
56
|
+
*/
|
|
57
|
+
issueToken(args: IssueTokenArgs): void;
|
|
58
|
+
/**
|
|
59
|
+
* Atomic consume. Single SQL UPDATE, returns whether exactly one row
|
|
60
|
+
* transitioned. When consumed=true the row's email/peerId/threadId are
|
|
61
|
+
* returned for the caller to mint the visitor token.
|
|
62
|
+
*
|
|
63
|
+
* Per spec fix #8 — the entire decision lives in `changes()`, no race.
|
|
64
|
+
*/
|
|
65
|
+
consumeToken(token: string, now: number): ConsumeTokenResult;
|
|
66
|
+
/**
|
|
67
|
+
* Read-only status query. Used by the verify route to disambiguate
|
|
68
|
+
* 410 (consumed/expired) from 404 (unknown) after a failed consumeToken.
|
|
69
|
+
*/
|
|
70
|
+
tokenStatus(token: string, now: number): TokenStatus;
|
|
71
|
+
/**
|
|
72
|
+
* The most-recent OPEN (unconsumed, unexpired) token for this peer, if any.
|
|
73
|
+
* Used by `request_auth` to invalidate prior open tokens before issuing a new one.
|
|
74
|
+
*/
|
|
75
|
+
findOpenTokenForPeer(peerId: string, now: number): OpenTokenForPeer | null;
|
|
76
|
+
/**
|
|
77
|
+
* The most-recent token for this peer regardless of consumed/expired status.
|
|
78
|
+
* Used by context() to surface "verification expired" state. Returns null
|
|
79
|
+
* if the peer has never had a token issued.
|
|
80
|
+
*/
|
|
81
|
+
findMostRecentTokenForPeer(
|
|
82
|
+
peerId: string,
|
|
83
|
+
now: number,
|
|
84
|
+
): {
|
|
85
|
+
email: string;
|
|
86
|
+
expiresAt: number;
|
|
87
|
+
issuedAt: number;
|
|
88
|
+
consumed: boolean;
|
|
89
|
+
} | null;
|
|
90
|
+
/**
|
|
91
|
+
* Mark every open token for this peer as consumed (without minting a
|
|
92
|
+
* visitor token). Used when a peer requests a new email; the prior code
|
|
93
|
+
* goes dead.
|
|
94
|
+
*/
|
|
95
|
+
invalidateOpenTokensForPeer(peerId: string, now: number): number;
|
|
96
|
+
/**
|
|
97
|
+
* Mark exactly one token as consumed if it is still open. Returns true
|
|
98
|
+
* if a row was modified (the token existed and was unconsumed), false
|
|
99
|
+
* otherwise. Used by the request_auth failure-path cleanup so a failed
|
|
100
|
+
* send only invalidates its OWN token — without this, a concurrent
|
|
101
|
+
* request_auth call from the same peer could be invalidated as
|
|
102
|
+
* collateral when the failed call's cleanup runs (F3).
|
|
103
|
+
*/
|
|
104
|
+
invalidateTokenIfStillOpen(token: string, now: number): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Insert a verified-visitor row. Caller has already minted the visitor token.
|
|
107
|
+
* If a row with the same email exists and is not revoked, throws — caller
|
|
108
|
+
* should treat this as "already verified, prefer existing identity"
|
|
109
|
+
* (handled in the verify route).
|
|
110
|
+
*/
|
|
111
|
+
recordVerifiedVisitor(row: VerifiedVisitorRow): void;
|
|
112
|
+
/** Returns the row for an email, or null. */
|
|
113
|
+
findVerifiedByEmail(email: string): VerifiedVisitorRow | null;
|
|
114
|
+
/** Update lastSeenAt; no-op if email is unknown or revoked. */
|
|
115
|
+
touchVerifiedVisitor(email: string, now: number): void;
|
|
116
|
+
/** All verified-visitor rows, ordered by verifiedAt DESC. Used by `auggy visitors`. */
|
|
117
|
+
listVerifiedVisitors(): VerifiedVisitorRow[];
|
|
118
|
+
/**
|
|
119
|
+
* Hard-revoke. Sets revoked=1 + reason. Returns the visitorId or null
|
|
120
|
+
* if the email was unknown. Used by `auggy visitors --revoke`.
|
|
121
|
+
*/
|
|
122
|
+
revokeByEmail(email: string, reason: string, now: number): string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Lookup a verified-visitor row by visitorId. Returns null if no row with
|
|
125
|
+
* that id exists. Used by `isVisitorRevoked` to enable real-time revocation
|
|
126
|
+
* checks at webTransport ingress (fix C1).
|
|
127
|
+
*/
|
|
128
|
+
findVisitorById(visitorId: string): VerifiedVisitorRow | null;
|
|
129
|
+
/**
|
|
130
|
+
* Un-revoke a previously-revoked row and rotate to a fresh visitorId.
|
|
131
|
+
* Called by the verify route when a visitor re-verifies after their identity
|
|
132
|
+
* was revoked. A single UPDATE atomically clears revocation state, writes the
|
|
133
|
+
* new vis_<uuid>, and updates verifiedAt / lastSeenAt / reverifyDueAt.
|
|
134
|
+
*
|
|
135
|
+
* Returns true iff exactly one revoked row was updated. Returns false if the
|
|
136
|
+
* email is unknown or the row is not revoked (no-op guard).
|
|
137
|
+
*/
|
|
138
|
+
unrevokeAndRotate(
|
|
139
|
+
email: string,
|
|
140
|
+
newVisitorId: string,
|
|
141
|
+
verifiedAt: number,
|
|
142
|
+
reverifyDueAt: number,
|
|
143
|
+
): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Permanently record a visitor_id as revoked. The denylist survives even
|
|
146
|
+
* when the row is rotated by unrevokeAndRotate (which rewrites visitor_id),
|
|
147
|
+
* so old tokens cannot bypass the revocation check by returning a row that
|
|
148
|
+
* no longer carries the old id.
|
|
149
|
+
*
|
|
150
|
+
* INSERT OR IGNORE — idempotent; second call with the same id is a no-op.
|
|
151
|
+
*/
|
|
152
|
+
addRevokedVisitorId(visitorId: string, email: string, reason: string, now: number): void;
|
|
153
|
+
/**
|
|
154
|
+
* Returns true iff the given visitor_id appears in the permanent denylist.
|
|
155
|
+
* Used by isVisitorRevoked in index.ts as the first check before falling
|
|
156
|
+
* through to the live row lookup.
|
|
157
|
+
*/
|
|
158
|
+
isVisitorIdRevoked(visitorId: string): boolean;
|
|
159
|
+
/** True iff the augment has emitted notifyOnFirstVerify for this email yet. */
|
|
160
|
+
hasNotifiedFirstVerifyFor(email: string): boolean;
|
|
161
|
+
/** Mark notifyOnFirstVerify as fired for this email. Idempotent. */
|
|
162
|
+
markNotifiedFirstVerifyFor(email: string, now: number): void;
|
|
163
|
+
close(): void;
|
|
164
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the visitorAuth augment.
|
|
3
|
+
*
|
|
4
|
+
* Exposed to the auggy resolver (consumes VisitorAuthOptions) and to the
|
|
5
|
+
* augment's internal modules. All shapes here are stable contracts; storage
|
|
6
|
+
* record shapes live in storage/types.ts (a deliberate split — operator-facing
|
|
7
|
+
* config is separate from on-disk representation).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AgentMail delivery configuration. visitorAuth uses src/agentmail-client.ts
|
|
12
|
+
* directly (see Plan §"Spec deviation"). Operator wires apiKey + inboxId via
|
|
13
|
+
* env-var interpolation in agent.yaml.
|
|
14
|
+
*/
|
|
15
|
+
export interface AgentMailConfig {
|
|
16
|
+
/** Bearer token (`am_*` prefix). Resolve via `${AGENTMAIL_API_KEY}` in agent.yaml. */
|
|
17
|
+
apiKey: string;
|
|
18
|
+
/** AgentMail inbox the verify email is sent FROM. */
|
|
19
|
+
inboxId: string;
|
|
20
|
+
/** Optional subject prefix prepended to the templated subject. Default: `[Verify] `. */
|
|
21
|
+
subjectPrefix?: string;
|
|
22
|
+
/** Optional override for the AgentMail API base URL (testing/sandbox). */
|
|
23
|
+
apiBaseUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-anonymous-peer rate-limit caps for `request_auth` calls. Defaults:
|
|
28
|
+
* 1 send per hour, 3 sends per 24 hours. State is in-memory (resets on
|
|
29
|
+
* restart — documented behavior; the verified_visitors UNIQUE-on-email
|
|
30
|
+
* constraint catches accidental double-verification).
|
|
31
|
+
*/
|
|
32
|
+
export interface VisitorAuthRateLimit {
|
|
33
|
+
perHour: number;
|
|
34
|
+
perDay: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Operator notification fired the FIRST time an email verifies on this agent.
|
|
39
|
+
* Optional; when set, visitorAuth uses agentmail-client to send a one-line
|
|
40
|
+
* note from inboxId TO the operator address. Independent from `notify`.
|
|
41
|
+
*/
|
|
42
|
+
export interface NotifyOnFirstVerifyConfig {
|
|
43
|
+
to: string;
|
|
44
|
+
/** Optional subject prefix (default `[New verified visitor] `). */
|
|
45
|
+
subjectPrefix?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface VisitorAuthOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Public-facing base URL for the magic link, e.g. `https://zip.lorf.dev`.
|
|
51
|
+
* Must be a valid URL with `http://` or `https://` scheme. Required because
|
|
52
|
+
* the magic-link URL embedded in the email is `<publicUrl>/visitor-auth/verify?token=<uuid>`.
|
|
53
|
+
*/
|
|
54
|
+
publicUrl: string;
|
|
55
|
+
/** Path to the visitor-auth SQLite database. Default: `./visitor-auth.db` (relative to agent dir). */
|
|
56
|
+
dbPath: string;
|
|
57
|
+
/** AgentMail delivery config. Required. */
|
|
58
|
+
agentMail: AgentMailConfig;
|
|
59
|
+
/**
|
|
60
|
+
* HMAC signing key for minting `vis_<uuid>` visitor tokens after a successful
|
|
61
|
+
* verify. MUST match webTransport's `visitorTokens.signingKey`. Resolve via
|
|
62
|
+
* `${VISITOR_SIGNING_KEY}` in agent.yaml (same env var both augments read).
|
|
63
|
+
*/
|
|
64
|
+
signingKey: string;
|
|
65
|
+
/**
|
|
66
|
+
* Stable identifier this agent uses for visitor tokens (fix C2). MUST match
|
|
67
|
+
* webTransport's `visitorTokens.agentBinding`. Default: `"auggy"`.
|
|
68
|
+
*
|
|
69
|
+
* When two agents share the same signing key, setting distinct `agentBinding`
|
|
70
|
+
* values on each prevents cross-agent replay: a token minted for agent A
|
|
71
|
+
* will be rejected by agent B because the embedded agentId will not match
|
|
72
|
+
* agent B's expected binding.
|
|
73
|
+
*/
|
|
74
|
+
agentBinding?: string;
|
|
75
|
+
/** Optional rate-limit caps. Defaults: { perHour: 1, perDay: 3 }. */
|
|
76
|
+
rateLimit?: VisitorAuthRateLimit;
|
|
77
|
+
/** Days before reverification is required. Default: 90. */
|
|
78
|
+
reverifyAfterDays?: number;
|
|
79
|
+
/** Token TTL in minutes. Default: 15. */
|
|
80
|
+
tokenTtlMinutes?: number;
|
|
81
|
+
/** Optional operator-notification on first verify per email. */
|
|
82
|
+
notifyOnFirstVerify?: NotifyOnFirstVerifyConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Path to the layeredMemory SQLite database for the anonymous→recognized
|
|
85
|
+
* peer-id migration on successful verify. Default: `./memory.db` (relative
|
|
86
|
+
* to agent dir). Set to `null` to disable migration (anonymous history will
|
|
87
|
+
* be orphaned but still queryable by threadId).
|
|
88
|
+
*/
|
|
89
|
+
layeredMemoryDbPath?: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extra surface exposed by the visitorAuth augment beyond the base Augment
|
|
94
|
+
* interface. Consumed by the augment resolver to wire the revocation check
|
|
95
|
+
* into webTransport without requiring a new kernel surface (fix C1).
|
|
96
|
+
*/
|
|
97
|
+
export interface VisitorAuthAugmentExtras {
|
|
98
|
+
/**
|
|
99
|
+
* Returns `true` iff the visitor with the given `vis_<uuid>` id has been
|
|
100
|
+
* revoked. Reads directly from the store — no caching, always current.
|
|
101
|
+
* Intended to be wired as `webTransport.visitorTokens.revocationCheck`.
|
|
102
|
+
*/
|
|
103
|
+
isVisitorRevoked(visitorId: string): boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Return shape of `request_auth({...})`. JSON-stringified by the tool. */
|
|
107
|
+
export interface RequestAuthResult {
|
|
108
|
+
status: "sent" | "rejected" | "failed";
|
|
109
|
+
message: string;
|
|
110
|
+
/** Present iff status === "sent". TTL of the issued token. */
|
|
111
|
+
expiresInSec?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Snapshot of the most-recent visitor message text the augment uses for
|
|
116
|
+
* the email-in-recent-message validation. The transcript itself lives
|
|
117
|
+
* in the kernel; visitorAuth only needs the visitor's recent text.
|
|
118
|
+
*/
|
|
119
|
+
export interface RecentVisitorMessage {
|
|
120
|
+
text: string;
|
|
121
|
+
/** Optional message id; recorded with the token for audit. */
|
|
122
|
+
messageId?: string;
|
|
123
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure HTML builders for the verify-success + verify-failure pages.
|
|
3
|
+
*
|
|
4
|
+
* Security posture (spec fix #5):
|
|
5
|
+
* - <meta name="referrer" content="no-referrer">
|
|
6
|
+
* - Zero external assets (inline CSS, no fonts, no analytics, no images)
|
|
7
|
+
* - history.replaceState fires on load to drop the token from the URL bar
|
|
8
|
+
* before any browser history snapshot
|
|
9
|
+
* - The visitor token is JSON-encoded and </script>-escaped before being
|
|
10
|
+
* written into the inline <script> block
|
|
11
|
+
* - The email is rendered via document.createTextNode (innerText), not innerHTML
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface VerifyConfirmPageInput {
|
|
15
|
+
token: string;
|
|
16
|
+
publicUrl: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VerifySuccessPageInput {
|
|
20
|
+
visitorToken: string;
|
|
21
|
+
email: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VerifyFailurePageInput {
|
|
25
|
+
reason: "expired" | "consumed" | "unknown" | "malformed" | string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function htmlEscape(value: string): string {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/&/g, "&")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/"/g, """)
|
|
34
|
+
.replace(/'/g, "'");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* JSON-encode a string for safe interpolation inside an inline <script>.
|
|
39
|
+
* The U+003C escape neutralizes any embedded `</script>` so the script block
|
|
40
|
+
* cannot be terminated by attacker-controlled content.
|
|
41
|
+
*/
|
|
42
|
+
function jsStringLiteral(value: string): string {
|
|
43
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const COMMON_HEAD = `<!DOCTYPE html>
|
|
47
|
+
<html lang="en">
|
|
48
|
+
<head>
|
|
49
|
+
<meta charset="utf-8">
|
|
50
|
+
<meta name="referrer" content="no-referrer">
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
52
|
+
<title>Auggy — verification</title>
|
|
53
|
+
<style>
|
|
54
|
+
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
55
|
+
body { margin: 0; padding: 3rem 1rem; max-width: 36rem; margin-inline: auto; line-height: 1.5; }
|
|
56
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
|
|
57
|
+
p { margin: 0.5rem 0; color: #555; }
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
body { background: #111; color: #eee; }
|
|
60
|
+
p { color: #aaa; }
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
</head>`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Confirmation page returned by GET /visitor-auth/verify.
|
|
67
|
+
*
|
|
68
|
+
* Mail scanners follow links passively (GET); they do NOT auto-submit forms.
|
|
69
|
+
* Returning this page on GET means the scanner harmlessly receives the
|
|
70
|
+
* confirmation page without consuming the one-time token. The human clicks
|
|
71
|
+
* "Verify my email" which triggers a form POST that actually consumes it.
|
|
72
|
+
*
|
|
73
|
+
* Security notes:
|
|
74
|
+
* - <meta name="referrer" content="no-referrer"> prevents the token from
|
|
75
|
+
* leaking in Referer headers to third parties.
|
|
76
|
+
* - The form POSTs to an absolute URL (publicUrl) to guarantee same-site
|
|
77
|
+
* submission even if the user opened the link on a different device.
|
|
78
|
+
* - Inline JS disables the button after click to prevent accidental double-
|
|
79
|
+
* submit within the same page load.
|
|
80
|
+
* - NO localStorage write, NO history.replaceState — those happen on the
|
|
81
|
+
* success page served after the POST.
|
|
82
|
+
*/
|
|
83
|
+
export function buildVerifyConfirmPage(input: VerifyConfirmPageInput): string {
|
|
84
|
+
const base = input.publicUrl.endsWith("/") ? input.publicUrl.slice(0, -1) : input.publicUrl;
|
|
85
|
+
const actionUrl = `${base}/visitor-auth/verify`;
|
|
86
|
+
// Token comes from validated UUID input — safe to embed as a hidden field value.
|
|
87
|
+
// htmlEscape is applied for correctness even though UUIDs are [0-9a-f-] only.
|
|
88
|
+
const safeToken = htmlEscape(input.token);
|
|
89
|
+
return `${COMMON_HEAD}
|
|
90
|
+
<body>
|
|
91
|
+
<h1>Verify your email</h1>
|
|
92
|
+
<p>Click the button below to complete email verification. This link can only be used once.</p>
|
|
93
|
+
<form method="POST" action="${htmlEscape(actionUrl)}" id="vf">
|
|
94
|
+
<input type="hidden" name="token" value="${safeToken}">
|
|
95
|
+
<button type="submit" id="btn" style="font-size:1rem;padding:0.6rem 1.4rem;cursor:pointer;">Verify my email</button>
|
|
96
|
+
</form>
|
|
97
|
+
<script>
|
|
98
|
+
(function(){
|
|
99
|
+
var btn = document.getElementById('btn');
|
|
100
|
+
var form = document.getElementById('vf');
|
|
101
|
+
if (form && btn) {
|
|
102
|
+
form.addEventListener('submit', function(){
|
|
103
|
+
btn.disabled = true;
|
|
104
|
+
btn.textContent = 'Verifying…';
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
</script>
|
|
109
|
+
<noscript><p>Submit the form above to verify your email.</p></noscript>
|
|
110
|
+
</body>
|
|
111
|
+
</html>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildVerifySuccessPage(input: VerifySuccessPageInput): string {
|
|
115
|
+
const tokenLit = jsStringLiteral(input.visitorToken);
|
|
116
|
+
const emailLit = jsStringLiteral(input.email);
|
|
117
|
+
return `${COMMON_HEAD}
|
|
118
|
+
<body>
|
|
119
|
+
<h1 id="title">Verifying…</h1>
|
|
120
|
+
<p id="msg">Please wait.</p>
|
|
121
|
+
<p id="storage-fallback" style="display:none">
|
|
122
|
+
Verified, but your browser blocked storage access. Copy this token manually to your chat tab:
|
|
123
|
+
<br><code id="manual-token" style="word-break:break-all"></code>
|
|
124
|
+
<br><small>(This may happen in private/incognito mode or sandboxed iframes.)</small>
|
|
125
|
+
</p>
|
|
126
|
+
<script>
|
|
127
|
+
(function () {
|
|
128
|
+
var token = ${tokenLit};
|
|
129
|
+
var email = ${emailLit};
|
|
130
|
+
var storageWorks = false;
|
|
131
|
+
try {
|
|
132
|
+
localStorage.setItem('auggy-visitor-token', token);
|
|
133
|
+
storageWorks = true;
|
|
134
|
+
} catch (_) { /* storage may be denied in private/incognito mode or sandboxed iframes */ }
|
|
135
|
+
try {
|
|
136
|
+
history.replaceState(null, '', './verified');
|
|
137
|
+
} catch (_) { /* older browsers — best-effort */ }
|
|
138
|
+
var titleEl = document.getElementById('title');
|
|
139
|
+
var msgEl = document.getElementById('msg');
|
|
140
|
+
var fallbackEl = document.getElementById('storage-fallback');
|
|
141
|
+
var manualEl = document.getElementById('manual-token');
|
|
142
|
+
if (titleEl) titleEl.textContent = 'Verified.';
|
|
143
|
+
if (storageWorks) {
|
|
144
|
+
if (msgEl) {
|
|
145
|
+
msgEl.textContent = 'Email verified: ' + email + '. You may close this tab; your chat tab will pick up the new identity on its next message. If you opened this link on a different device, refresh your chat tab.';
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
if (msgEl) msgEl.style.display = 'none';
|
|
149
|
+
if (fallbackEl) fallbackEl.style.display = '';
|
|
150
|
+
if (manualEl) manualEl.textContent = token;
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
</script>
|
|
154
|
+
<noscript>
|
|
155
|
+
<p>Email verified, but JavaScript is required to apply the new identity to your chat tab. Please re-open your chat tab manually.</p>
|
|
156
|
+
</noscript>
|
|
157
|
+
</body>
|
|
158
|
+
</html>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const FAILURE_COPY: Record<string, string> = {
|
|
162
|
+
expired: "This verification link has expired. Please ask the agent to send a new one.",
|
|
163
|
+
consumed:
|
|
164
|
+
"This verification link has already been used. If you didn't expect this, request a new link.",
|
|
165
|
+
unknown: "We don't recognize this verification link. It may be malformed or out of date.",
|
|
166
|
+
malformed: "This verification link is malformed.",
|
|
167
|
+
"bad-body": "The verification request was malformed (could not parse the request body).",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export function buildVerifyFailurePage(input: VerifyFailurePageInput): string {
|
|
171
|
+
const known = FAILURE_COPY[input.reason];
|
|
172
|
+
const safeReason = known ?? "Verification failed.";
|
|
173
|
+
return `${COMMON_HEAD}
|
|
174
|
+
<body>
|
|
175
|
+
<h1>Verification failed</h1>
|
|
176
|
+
<p>${htmlEscape(safeReason)}</p>
|
|
177
|
+
</body>
|
|
178
|
+
</html>`;
|
|
179
|
+
}
|