@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { getAmplifyDataClientConfig } from "@aws-amplify/backend/function/runtime";
|
|
3
|
+
import { Amplify } from "aws-amplify";
|
|
4
|
+
import { generateClient } from "aws-amplify/data";
|
|
5
|
+
import type { Schema } from "../../data/resource";
|
|
6
|
+
|
|
7
|
+
type GetSettingsHandler = Schema["getReaderSettings"]["functionHandler"];
|
|
8
|
+
type UpdateSettingsHandler = Schema["updateReaderSettings"]["functionHandler"];
|
|
9
|
+
type ReaderSettingsEvent = Parameters<GetSettingsHandler>[0] | Parameters<UpdateSettingsHandler>[0];
|
|
10
|
+
type DataClient = ReturnType<typeof generateClient<Schema>>;
|
|
11
|
+
type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
|
|
12
|
+
type DataClientResult<T = unknown> = {
|
|
13
|
+
data?: T | null;
|
|
14
|
+
errors?: DataClientErrors;
|
|
15
|
+
nextToken?: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ReaderSettings = {
|
|
19
|
+
presentation: "newsprint" | "blog" | "magazine";
|
|
20
|
+
theme: "system" | "light" | "dark";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UserProfileRecord = {
|
|
24
|
+
id: string;
|
|
25
|
+
email?: string | null;
|
|
26
|
+
displayName?: string | null;
|
|
27
|
+
settings?: unknown;
|
|
28
|
+
status?: string | null;
|
|
29
|
+
mergedIntoProfileId?: string | null;
|
|
30
|
+
createdAt?: string | null;
|
|
31
|
+
updatedAt?: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type UserIdentityRecord = {
|
|
35
|
+
id: string;
|
|
36
|
+
userProfileId: string;
|
|
37
|
+
cognitoSub: string;
|
|
38
|
+
provider?: string | null;
|
|
39
|
+
email?: string | null;
|
|
40
|
+
status: string;
|
|
41
|
+
linkedAt: string;
|
|
42
|
+
lastSeenAt?: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ACTIVE_IDENTITY_STATUS = "active";
|
|
46
|
+
const DEFAULT_SETTINGS: ReaderSettings = {
|
|
47
|
+
presentation: "newsprint",
|
|
48
|
+
theme: "system",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let clientPromise: Promise<DataClient> | null = null;
|
|
52
|
+
|
|
53
|
+
export const handler = async (event: ReaderSettingsEvent) => {
|
|
54
|
+
const client = await getDataClient();
|
|
55
|
+
const operation = getOperationName(event);
|
|
56
|
+
const identity = readRequiredIdentity(event);
|
|
57
|
+
const profile = await resolveReaderProfile(client, identity);
|
|
58
|
+
|
|
59
|
+
if (operation === "getReaderSettings") {
|
|
60
|
+
return {
|
|
61
|
+
userProfileId: profile.id,
|
|
62
|
+
email: profile.email ?? null,
|
|
63
|
+
settings: hasStoredReaderSettings(profile.settings) ? normalizeReaderSettings(profile.settings) : null,
|
|
64
|
+
avatarHash: computeAvatarHash(profile.email),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (operation === "updateReaderSettings") {
|
|
69
|
+
if (!("settings" in event.arguments)) throw new Error("settings is required.");
|
|
70
|
+
const nextSettings = normalizeReaderSettings(event.arguments.settings);
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
await requireDataResult(
|
|
73
|
+
client.models.UserProfile.update({
|
|
74
|
+
id: profile.id,
|
|
75
|
+
settings: nextSettings,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
}),
|
|
78
|
+
"update UserProfile reader settings",
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
userProfileId: profile.id,
|
|
82
|
+
email: profile.email ?? null,
|
|
83
|
+
settings: nextSettings,
|
|
84
|
+
avatarHash: computeAvatarHash(profile.email),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(`Unsupported reader settings operation ${operation}.`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function getOperationName(event: ReaderSettingsEvent): string {
|
|
92
|
+
const operation = normalizeOptionalString(event.info?.fieldName) ?? normalizeOptionalString((event as { fieldName?: string | null }).fieldName);
|
|
93
|
+
if (!operation) throw new Error("Reader settings operation event did not include a field name.");
|
|
94
|
+
return operation;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function resolveReaderProfile(
|
|
98
|
+
client: DataClient,
|
|
99
|
+
identity: {
|
|
100
|
+
sub: string;
|
|
101
|
+
email?: string | null;
|
|
102
|
+
displayName?: string | null;
|
|
103
|
+
provider?: string | null;
|
|
104
|
+
},
|
|
105
|
+
): Promise<UserProfileRecord> {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
const identities = await listDataRecords<UserIdentityRecord>(client.models.UserIdentity);
|
|
108
|
+
const existingIdentity = identities.find((record) => (
|
|
109
|
+
record.cognitoSub === identity.sub && record.status === ACTIVE_IDENTITY_STATUS
|
|
110
|
+
));
|
|
111
|
+
|
|
112
|
+
if (existingIdentity) {
|
|
113
|
+
await requireDataResult(
|
|
114
|
+
client.models.UserIdentity.update({
|
|
115
|
+
id: existingIdentity.id,
|
|
116
|
+
lastSeenAt: now,
|
|
117
|
+
}),
|
|
118
|
+
"update UserIdentity lastSeenAt",
|
|
119
|
+
);
|
|
120
|
+
let profile = await getDataRecord<UserProfileRecord>(client.models.UserProfile, existingIdentity.userProfileId, "get UserProfile");
|
|
121
|
+
if (!profile) throw new Error(`UserProfile ${existingIdentity.userProfileId} was not found.`);
|
|
122
|
+
if (profile.mergedIntoProfileId) {
|
|
123
|
+
const mergedTarget = await getDataRecord<UserProfileRecord>(client.models.UserProfile, profile.mergedIntoProfileId, "get merged UserProfile");
|
|
124
|
+
if (mergedTarget) return mergedTarget;
|
|
125
|
+
}
|
|
126
|
+
const nextEmail = identity.email ?? null;
|
|
127
|
+
const currentEmail = normalizeOptionalString(profile.email);
|
|
128
|
+
if (nextEmail && nextEmail !== currentEmail) {
|
|
129
|
+
profile = await requireDataResult(
|
|
130
|
+
client.models.UserProfile.update({
|
|
131
|
+
id: profile.id,
|
|
132
|
+
email: nextEmail,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
}),
|
|
135
|
+
"update UserProfile email",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return profile;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const profileId = `user-profile-${safeId(identity.sub)}`;
|
|
142
|
+
const existingProfile = await getDataRecord<UserProfileRecord>(client.models.UserProfile, profileId, "get UserProfile");
|
|
143
|
+
const profile = existingProfile
|
|
144
|
+
? await maybeRefreshProfileFromIdentity(client, existingProfile, identity, now)
|
|
145
|
+
: await requireDataResult(
|
|
146
|
+
client.models.UserProfile.create({
|
|
147
|
+
id: profileId,
|
|
148
|
+
email: identity.email ?? null,
|
|
149
|
+
displayName: identity.displayName ?? identity.email ?? "Papyrus reader",
|
|
150
|
+
status: ACTIVE_IDENTITY_STATUS,
|
|
151
|
+
createdAt: now,
|
|
152
|
+
updatedAt: now,
|
|
153
|
+
}),
|
|
154
|
+
"create UserProfile",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await requireDataResult(
|
|
158
|
+
client.models.UserIdentity.create({
|
|
159
|
+
id: `user-identity-${safeId(identity.sub)}-${randomUUID().slice(0, 8)}`,
|
|
160
|
+
userProfileId: profileId,
|
|
161
|
+
cognitoSub: identity.sub,
|
|
162
|
+
provider: identity.provider ?? null,
|
|
163
|
+
email: identity.email ?? null,
|
|
164
|
+
status: ACTIVE_IDENTITY_STATUS,
|
|
165
|
+
linkedAt: now,
|
|
166
|
+
lastSeenAt: now,
|
|
167
|
+
}),
|
|
168
|
+
"create UserIdentity",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return profile;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function maybeRefreshProfileFromIdentity(
|
|
175
|
+
client: DataClient,
|
|
176
|
+
profile: UserProfileRecord,
|
|
177
|
+
identity: {
|
|
178
|
+
sub: string;
|
|
179
|
+
email?: string | null;
|
|
180
|
+
displayName?: string | null;
|
|
181
|
+
provider?: string | null;
|
|
182
|
+
},
|
|
183
|
+
now: string,
|
|
184
|
+
): Promise<UserProfileRecord> {
|
|
185
|
+
const currentEmail = normalizeOptionalString(profile.email);
|
|
186
|
+
const nextEmail = normalizeOptionalString(identity.email);
|
|
187
|
+
const nextDisplayName = normalizeOptionalString(identity.displayName);
|
|
188
|
+
const patch: {
|
|
189
|
+
id: string;
|
|
190
|
+
email?: string | null;
|
|
191
|
+
displayName?: string | null;
|
|
192
|
+
updatedAt: string;
|
|
193
|
+
} = {
|
|
194
|
+
id: profile.id,
|
|
195
|
+
updatedAt: now,
|
|
196
|
+
};
|
|
197
|
+
let changed = false;
|
|
198
|
+
if (nextEmail && nextEmail !== currentEmail) {
|
|
199
|
+
patch.email = nextEmail;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (nextDisplayName && nextDisplayName !== normalizeOptionalString(profile.displayName)) {
|
|
203
|
+
patch.displayName = nextDisplayName;
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
if (!changed) return profile;
|
|
207
|
+
return requireDataResult(
|
|
208
|
+
client.models.UserProfile.update(patch),
|
|
209
|
+
"refresh UserProfile identity fields",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeReaderSettings(value: unknown): ReaderSettings {
|
|
214
|
+
const candidate = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
215
|
+
return {
|
|
216
|
+
presentation: normalizePresentation(candidate.presentation),
|
|
217
|
+
theme: normalizeTheme(candidate.theme),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasStoredReaderSettings(value: unknown): boolean {
|
|
222
|
+
return Boolean(value && typeof value === "object");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizePresentation(value: unknown): ReaderSettings["presentation"] {
|
|
226
|
+
if (value === "blog" || value === "magazine" || value === "newsprint") return value;
|
|
227
|
+
// Handle deprecated "newspaper" value as an alias for "newsprint"
|
|
228
|
+
if (value === "newspaper") return "newsprint";
|
|
229
|
+
return DEFAULT_SETTINGS.presentation;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeTheme(value: unknown): ReaderSettings["theme"] {
|
|
233
|
+
return value === "light" || value === "dark" || value === "system" ? value : DEFAULT_SETTINGS.theme;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function computeAvatarHash(email: string | null | undefined): string | null {
|
|
237
|
+
const normalized = normalizeOptionalString(email)?.toLowerCase();
|
|
238
|
+
if (!normalized) return null;
|
|
239
|
+
return createHash("md5").update(normalized).digest("hex");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readRequiredIdentity(event: ReaderSettingsEvent): {
|
|
243
|
+
sub: string;
|
|
244
|
+
email?: string | null;
|
|
245
|
+
displayName?: string | null;
|
|
246
|
+
provider?: string | null;
|
|
247
|
+
} {
|
|
248
|
+
const identity = event.identity as { sub?: unknown; username?: unknown; claims?: Record<string, unknown> } | null | undefined;
|
|
249
|
+
const claims = identity?.claims ?? {};
|
|
250
|
+
const sub = normalizeOptionalString(identity?.sub) ?? normalizeOptionalString(claims.sub);
|
|
251
|
+
if (!sub) throw new Error("Reader settings require an authenticated Cognito user.");
|
|
252
|
+
return {
|
|
253
|
+
sub,
|
|
254
|
+
email: normalizeOptionalString(claims.email),
|
|
255
|
+
displayName: normalizeOptionalString(claims.name) ?? normalizeOptionalString(claims["cognito:username"]) ?? normalizeOptionalString(identity?.username),
|
|
256
|
+
provider: normalizeProvider(claims.identities),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeProvider(value: unknown): string | null {
|
|
261
|
+
if (!Array.isArray(value)) return null;
|
|
262
|
+
const first = value.find((entry) => entry && typeof entry === "object") as Record<string, unknown> | undefined;
|
|
263
|
+
return normalizeOptionalString(first?.providerName);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function getDataClient(): Promise<DataClient> {
|
|
267
|
+
if (!clientPromise) {
|
|
268
|
+
clientPromise = (async () => {
|
|
269
|
+
const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(process.env as never);
|
|
270
|
+
Amplify.configure(resourceConfig, libraryOptions);
|
|
271
|
+
return generateClient<Schema>();
|
|
272
|
+
})();
|
|
273
|
+
}
|
|
274
|
+
return clientPromise;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function listDataRecords<T>(model: { list(input?: Record<string, unknown>): Promise<DataClientResult<T[]>> }): Promise<T[]> {
|
|
278
|
+
const records: T[] = [];
|
|
279
|
+
let nextToken: string | null | undefined;
|
|
280
|
+
do {
|
|
281
|
+
const page = await model.list({ limit: 100, nextToken });
|
|
282
|
+
assertNoDataErrors(page.errors, "list records");
|
|
283
|
+
records.push(...((page.data ?? []).filter(Boolean) as T[]));
|
|
284
|
+
nextToken = page.nextToken;
|
|
285
|
+
} while (nextToken);
|
|
286
|
+
return records;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function getDataRecord<T>(
|
|
290
|
+
model: { get(input: { id: string }): Promise<DataClientResult<T>> },
|
|
291
|
+
id: string,
|
|
292
|
+
operation: string,
|
|
293
|
+
): Promise<T | null> {
|
|
294
|
+
const response = await model.get({ id });
|
|
295
|
+
assertNoDataErrors(response.errors, operation);
|
|
296
|
+
return response.data ?? null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function requireDataResult<T>(promise: Promise<DataClientResult<T>>, operation: string): Promise<T> {
|
|
300
|
+
const response = await promise;
|
|
301
|
+
assertNoDataErrors(response.errors, operation);
|
|
302
|
+
if (!response.data) throw new Error(`${operation} returned no data.`);
|
|
303
|
+
return response.data;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function assertNoDataErrors(errors: DataClientErrors, operation: string): void {
|
|
307
|
+
if (!errors?.length) return;
|
|
308
|
+
throw new Error(`${operation} failed: ${errors.map(formatDataError).join("; ")}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatDataError(error: { message?: string | null } | string | null): string {
|
|
312
|
+
if (typeof error === "string") return error;
|
|
313
|
+
return error?.message ?? "GraphQL data operation failed.";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normalizeOptionalString(value: unknown): string | null {
|
|
317
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function safeId(value: string): string {
|
|
321
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
322
|
+
}
|