@aglyn/plugins-commerce 1.0.0-beta.277 → 1.0.0-beta.282
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/package.json +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- package/src/lib/testing/fake-firestore.js.map +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { createHash } from "node:crypto";
|
|
18
|
+
import { pluginRequestFromWeb } from "@aglyn/aglyn/server";
|
|
19
|
+
import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
|
|
20
|
+
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
21
|
+
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
22
|
+
import { POS_READER_SPLASH_FAMILIES, POS_READER_SPLASH_MAX_BYTES, posReaderSplashFamily, posReaderSplashImageProblem } from "../model/pos-reader-splash.js";
|
|
23
|
+
import { applyPosReaderSplash, ownerOrgOf, platformSplashReady, platformSplashRef, posReaderSplashBucket, readPlatformSplash } from "./pos-reader-splash.js";
|
|
24
|
+
import { posReaderSplashImageFacts } from "./pos-reader-splash-render.js";
|
|
25
|
+
/*==========================================
|
|
26
|
+
* `/api/commerce/admin/reader-splash` (AGL-4048): the platform's default
|
|
27
|
+
* card reader start-up screen, for Staff → Reader start-up screen.
|
|
28
|
+
*
|
|
29
|
+
* GET each family's screen, as metadata
|
|
30
|
+
* GET ?preview={family} that family's image itself
|
|
31
|
+
* POST upload { family, contentType, dataBase64 }
|
|
32
|
+
* POST remove { family }
|
|
33
|
+
* POST apply put the current default on every store that
|
|
34
|
+
* uses it (the page calls it after a change)
|
|
35
|
+
*
|
|
36
|
+
* The artwork is designed separately and uploaded here; none is kept in
|
|
37
|
+
* the repository. Each image is checked against Stripe's rules for its
|
|
38
|
+
* family before it is stored: PNG or JPEG, under 2 MB, exactly the reader's
|
|
39
|
+
* screen. The S700/S710 screen is the one that turns the default on; the
|
|
40
|
+
* WisePOS E screen is drawn from it when staff upload none.
|
|
41
|
+
*
|
|
42
|
+
* Staff only. Reading is open to every staff role; every write is `super`,
|
|
43
|
+
* because it changes what every non-white-label store's readers show. Every
|
|
44
|
+
* write is an `adminAudit` row. The images live in storage under
|
|
45
|
+
* `platform/commerce/pos-reader-splash/`, which no client rule opens.
|
|
46
|
+
*=========================================*/ const WRITE_ROLES = [
|
|
47
|
+
'super'
|
|
48
|
+
];
|
|
49
|
+
/** How many stores one `apply` reaches; staff press it again for the rest. */ const APPLY_WINDOW = 400;
|
|
50
|
+
const APPLY_CONCURRENCY = 4;
|
|
51
|
+
const json = (body, status = 200)=>Response.json(body, {
|
|
52
|
+
status,
|
|
53
|
+
headers: {
|
|
54
|
+
'Cache-Control': 'no-store'
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
function view(doc) {
|
|
58
|
+
var _doc_updatedAtMs;
|
|
59
|
+
return {
|
|
60
|
+
ready: platformSplashReady(doc),
|
|
61
|
+
families: POS_READER_SPLASH_FAMILIES.map((family)=>{
|
|
62
|
+
var _doc_families;
|
|
63
|
+
const image = (_doc_families = doc.families) == null ? void 0 : _doc_families[family.id];
|
|
64
|
+
return {
|
|
65
|
+
id: family.id,
|
|
66
|
+
label: family.label,
|
|
67
|
+
width: family.width,
|
|
68
|
+
height: family.height,
|
|
69
|
+
image: image ? {
|
|
70
|
+
contentType: image.contentType,
|
|
71
|
+
bytes: image.bytes,
|
|
72
|
+
sha256: image.sha256,
|
|
73
|
+
uploadedAtMs: image.uploadedAtMs
|
|
74
|
+
} : null
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
updatedAtMs: (_doc_updatedAtMs = doc.updatedAtMs) != null ? _doc_updatedAtMs : null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function upload(doc, input, uid) {
|
|
81
|
+
var _input_dataBase64;
|
|
82
|
+
var _doc_families;
|
|
83
|
+
const family = posReaderSplashFamily(input['family']);
|
|
84
|
+
if (!family) return {
|
|
85
|
+
error: 'Unknown reader family',
|
|
86
|
+
status: 400
|
|
87
|
+
};
|
|
88
|
+
const raw = String((_input_dataBase64 = input['dataBase64']) != null ? _input_dataBase64 : '');
|
|
89
|
+
// A base64 body is 4/3 of the file; anything longer cannot be under 2 MB.
|
|
90
|
+
if (!raw || raw.length > Math.ceil(POS_READER_SPLASH_MAX_BYTES * 4 / 3) + 4) {
|
|
91
|
+
return {
|
|
92
|
+
error: 'Use an image under 2 MB.',
|
|
93
|
+
status: 400
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const body = Buffer.from(raw, 'base64');
|
|
97
|
+
const { loadSharp } = await import("./pos-reader-splash-server.js");
|
|
98
|
+
const facts = await posReaderSplashImageFacts(await loadSharp(), body);
|
|
99
|
+
if (!facts) return {
|
|
100
|
+
error: 'That image could not be read.',
|
|
101
|
+
status: 400
|
|
102
|
+
};
|
|
103
|
+
// The bytes decide the type, never the name or the browser's claim.
|
|
104
|
+
const problem = posReaderSplashImageProblem(_extends({}, facts, {
|
|
105
|
+
bytes: body.length
|
|
106
|
+
}), family);
|
|
107
|
+
if (problem) return {
|
|
108
|
+
error: problem,
|
|
109
|
+
status: 400
|
|
110
|
+
};
|
|
111
|
+
const sha256 = createHash('sha256').update(body).digest('hex');
|
|
112
|
+
const extension = facts.contentType === 'image/png' ? 'png' : 'jpg';
|
|
113
|
+
const storagePath = `platform/commerce/pos-reader-splash/${family.id}-${sha256.slice(0, 16)}.${extension}`;
|
|
114
|
+
await posReaderSplashBucket().file(storagePath).save(body, {
|
|
115
|
+
contentType: facts.contentType,
|
|
116
|
+
resumable: false
|
|
117
|
+
});
|
|
118
|
+
const previous = (_doc_families = doc.families) == null ? void 0 : _doc_families[family.id];
|
|
119
|
+
if ((previous == null ? void 0 : previous.storagePath) && previous.storagePath !== storagePath) {
|
|
120
|
+
await posReaderSplashBucket().file(previous.storagePath).delete().catch(()=>undefined);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
familyId: family.id,
|
|
124
|
+
image: {
|
|
125
|
+
storagePath,
|
|
126
|
+
sha256,
|
|
127
|
+
contentType: facts.contentType,
|
|
128
|
+
bytes: body.length,
|
|
129
|
+
uploadedAtMs: Date.now(),
|
|
130
|
+
uploadedBy: uid
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Every store with a Location, its screen brought up to the current default. */ async function applyToEveryStore() {
|
|
135
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
136
|
+
const platform = await readPlatformSplash();
|
|
137
|
+
const snapshot = await firestore.collectionGroup('terminal').limit(APPLY_WINDOW + 1).get();
|
|
138
|
+
const hostIds = snapshot.docs.filter((doc)=>doc.id === 'config' && doc.get('stripeLocationId')).map((doc)=>doc.ref.path.split('/')[1]).filter(Boolean);
|
|
139
|
+
const counts = {
|
|
140
|
+
applied: 0,
|
|
141
|
+
unchanged: 0,
|
|
142
|
+
failed: 0
|
|
143
|
+
};
|
|
144
|
+
const queue = hostIds.slice(0, APPLY_WINDOW);
|
|
145
|
+
await Promise.all(Array.from({
|
|
146
|
+
length: APPLY_CONCURRENCY
|
|
147
|
+
}, async ()=>{
|
|
148
|
+
for(let hostId = queue.shift(); hostId; hostId = queue.shift()){
|
|
149
|
+
const org = await ownerOrgOf(hostId);
|
|
150
|
+
const result = await applyPosReaderSplash(hostId, {
|
|
151
|
+
org,
|
|
152
|
+
platform
|
|
153
|
+
});
|
|
154
|
+
if (result.outcome === 'applied' || result.outcome === 'cleared') counts.applied += 1;
|
|
155
|
+
else if (result.outcome === 'failed') counts.failed += 1;
|
|
156
|
+
else counts.unchanged += 1;
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
return _extends({}, counts, {
|
|
160
|
+
more: snapshot.docs.length > APPLY_WINDOW
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function handler(request) {
|
|
164
|
+
var _headers_authorization;
|
|
165
|
+
const { method, query, body, headers: rawHeaders } = await pluginRequestFromWeb(request);
|
|
166
|
+
const headers = rawHeaders;
|
|
167
|
+
const authorization = (_headers_authorization = headers.authorization) != null ? _headers_authorization : '';
|
|
168
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
169
|
+
if (!idToken) return json({
|
|
170
|
+
error: 'Unauthenticated'
|
|
171
|
+
}, 401);
|
|
172
|
+
try {
|
|
173
|
+
var _decoded_staffRole, _input_action;
|
|
174
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
175
|
+
if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse();
|
|
176
|
+
if (!decoded['staff']) return json({
|
|
177
|
+
error: 'Staff only'
|
|
178
|
+
}, 403);
|
|
179
|
+
// A missing role claim reads as the least one, never as super.
|
|
180
|
+
const role = String((_decoded_staffRole = decoded['staffRole']) != null ? _decoded_staffRole : 'support');
|
|
181
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
182
|
+
const doc = await readPlatformSplash();
|
|
183
|
+
if (method === 'GET') {
|
|
184
|
+
var _ref;
|
|
185
|
+
const preview = String((_ref = query == null ? void 0 : query['preview']) != null ? _ref : '');
|
|
186
|
+
if (preview) {
|
|
187
|
+
var _doc_families;
|
|
188
|
+
const family = posReaderSplashFamily(preview);
|
|
189
|
+
const image = family ? (_doc_families = doc.families) == null ? void 0 : _doc_families[family.id] : undefined;
|
|
190
|
+
if (!image) return json({
|
|
191
|
+
error: 'No image'
|
|
192
|
+
}, 404);
|
|
193
|
+
const [bytes] = await posReaderSplashBucket().file(image.storagePath).download();
|
|
194
|
+
return new Response(new Uint8Array(bytes), {
|
|
195
|
+
status: 200,
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': image.contentType,
|
|
198
|
+
'Cache-Control': 'no-store'
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return json(_extends({}, view(doc), {
|
|
203
|
+
canWrite: WRITE_ROLES.includes(role)
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
if (method !== 'POST') return json({
|
|
207
|
+
error: 'Method not allowed'
|
|
208
|
+
}, 405);
|
|
209
|
+
if (!WRITE_ROLES.includes(role)) {
|
|
210
|
+
return json({
|
|
211
|
+
error: 'Changing the reader start-up screen requires the super staff role.'
|
|
212
|
+
}, 403);
|
|
213
|
+
}
|
|
214
|
+
const input = body != null ? body : {};
|
|
215
|
+
const action = String((_input_action = input['action']) != null ? _input_action : '');
|
|
216
|
+
const actor = {
|
|
217
|
+
actorUid: decoded.uid,
|
|
218
|
+
actorEmail: decoded.email ? String(decoded.email) : null
|
|
219
|
+
};
|
|
220
|
+
if (action === 'apply') return json(_extends({
|
|
221
|
+
ok: true
|
|
222
|
+
}, await applyToEveryStore()));
|
|
223
|
+
if (action === 'upload' || action === 'remove') {
|
|
224
|
+
var _ref1, _ref2;
|
|
225
|
+
var _doc_families_familyId, _doc_families1, _next_families_familyId, _next_families;
|
|
226
|
+
let next;
|
|
227
|
+
let familyId;
|
|
228
|
+
if (action === 'upload') {
|
|
229
|
+
var _doc_families2;
|
|
230
|
+
const outcome = await upload(doc, input, decoded.uid);
|
|
231
|
+
if ('error' in outcome) return json({
|
|
232
|
+
error: outcome.error
|
|
233
|
+
}, outcome.status);
|
|
234
|
+
familyId = outcome.familyId;
|
|
235
|
+
next = {
|
|
236
|
+
families: _extends({}, (_doc_families2 = doc.families) != null ? _doc_families2 : {}, {
|
|
237
|
+
[familyId]: outcome.image
|
|
238
|
+
}),
|
|
239
|
+
updatedAtMs: Date.now()
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
var _doc_families3;
|
|
243
|
+
const family = posReaderSplashFamily(input['family']);
|
|
244
|
+
if (!family) return json({
|
|
245
|
+
error: 'Unknown reader family'
|
|
246
|
+
}, 400);
|
|
247
|
+
familyId = family.id;
|
|
248
|
+
const families = _extends({}, (_doc_families3 = doc.families) != null ? _doc_families3 : {});
|
|
249
|
+
const previous = families[family.id];
|
|
250
|
+
delete families[family.id];
|
|
251
|
+
if (previous == null ? void 0 : previous.storagePath) {
|
|
252
|
+
await posReaderSplashBucket().file(previous.storagePath).delete().catch(()=>undefined);
|
|
253
|
+
}
|
|
254
|
+
next = {
|
|
255
|
+
families,
|
|
256
|
+
updatedAtMs: Date.now()
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
await platformSplashRef().set(next);
|
|
260
|
+
await addAdminAudit(firestore, _extends({
|
|
261
|
+
action: `pos-reader-splash-${action}`
|
|
262
|
+
}, actor, {
|
|
263
|
+
scope: 'posReaderSplash',
|
|
264
|
+
target: `platformSettings/posReaderSplash`,
|
|
265
|
+
before: {
|
|
266
|
+
family: familyId,
|
|
267
|
+
sha256: (_ref1 = (_doc_families1 = doc.families) == null ? void 0 : (_doc_families_familyId = _doc_families1[familyId]) == null ? void 0 : _doc_families_familyId.sha256) != null ? _ref1 : null
|
|
268
|
+
},
|
|
269
|
+
after: {
|
|
270
|
+
family: familyId,
|
|
271
|
+
sha256: (_ref2 = (_next_families = next.families) == null ? void 0 : (_next_families_familyId = _next_families[familyId]) == null ? void 0 : _next_families_familyId.sha256) != null ? _ref2 : null
|
|
272
|
+
},
|
|
273
|
+
at: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
274
|
+
}));
|
|
275
|
+
return json(_extends({
|
|
276
|
+
ok: true
|
|
277
|
+
}, view(next), {
|
|
278
|
+
canWrite: true
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
return json({
|
|
282
|
+
error: 'Unknown action'
|
|
283
|
+
}, 400);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const unauthenticated = invalidIdTokenResponse(error);
|
|
286
|
+
if (unauthenticated) return unauthenticated;
|
|
287
|
+
console.error('[pos-reader-splash-admin]', error);
|
|
288
|
+
return json({
|
|
289
|
+
error: 'The reader start-up screen could not be updated'
|
|
290
|
+
}, 500);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
export { handler as posReaderSplashAdmin };
|
|
294
|
+
|
|
295
|
+
//# sourceMappingURL=pos-reader-splash-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-reader-splash-admin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n POS_READER_SPLASH_FAMILIES,\n POS_READER_SPLASH_MAX_BYTES,\n posReaderSplashFamily,\n posReaderSplashImageProblem,\n} from '../model/pos-reader-splash'\nimport {\n applyPosReaderSplash,\n ownerOrgOf,\n platformSplashReady,\n platformSplashRef,\n posReaderSplashBucket,\n readPlatformSplash,\n type PlatformSplashDoc,\n type PlatformSplashImage,\n} from './pos-reader-splash'\nimport { posReaderSplashImageFacts } from './pos-reader-splash-render'\n\n/*==========================================\n * `/api/commerce/admin/reader-splash` (AGL-4048): the platform's default\n * card reader start-up screen, for Staff → Reader start-up screen.\n *\n * GET each family's screen, as metadata\n * GET ?preview={family} that family's image itself\n * POST upload { family, contentType, dataBase64 }\n * POST remove { family }\n * POST apply put the current default on every store that\n * uses it (the page calls it after a change)\n *\n * The artwork is designed separately and uploaded here; none is kept in\n * the repository. Each image is checked against Stripe's rules for its\n * family before it is stored: PNG or JPEG, under 2 MB, exactly the reader's\n * screen. The S700/S710 screen is the one that turns the default on; the\n * WisePOS E screen is drawn from it when staff upload none.\n *\n * Staff only. Reading is open to every staff role; every write is `super`,\n * because it changes what every non-white-label store's readers show. Every\n * write is an `adminAudit` row. The images live in storage under\n * `platform/commerce/pos-reader-splash/`, which no client rule opens.\n *=========================================*/\n\nconst WRITE_ROLES = ['super']\n\n/** How many stores one `apply` reaches; staff press it again for the rest. */\nconst APPLY_WINDOW = 400\nconst APPLY_CONCURRENCY = 4\n\nconst json = (body: unknown, status = 200) =>\n Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } })\n\nfunction view(doc: PlatformSplashDoc) {\n return {\n ready: platformSplashReady(doc),\n families: POS_READER_SPLASH_FAMILIES.map((family) => {\n const image = doc.families?.[family.id]\n return {\n id: family.id,\n label: family.label,\n width: family.width,\n height: family.height,\n image: image\n ? {\n contentType: image.contentType,\n bytes: image.bytes,\n sha256: image.sha256,\n uploadedAtMs: image.uploadedAtMs,\n }\n : null,\n }\n }),\n updatedAtMs: doc.updatedAtMs ?? null,\n }\n}\n\nexport type StaffReaderSplashView = ReturnType<typeof view>\n\nasync function upload(\n doc: PlatformSplashDoc,\n input: Record<string, unknown>,\n uid: string,\n): Promise<{ error: string; status: number } | { image: PlatformSplashImage; familyId: string }> {\n const family = posReaderSplashFamily(input['family'])\n if (!family) return { error: 'Unknown reader family', status: 400 }\n const raw = String(input['dataBase64'] ?? '')\n // A base64 body is 4/3 of the file; anything longer cannot be under 2 MB.\n if (!raw || raw.length > Math.ceil((POS_READER_SPLASH_MAX_BYTES * 4) / 3) + 4) {\n return { error: 'Use an image under 2 MB.', status: 400 }\n }\n const body = Buffer.from(raw, 'base64')\n const { loadSharp } = await import('./pos-reader-splash-server')\n const facts = await posReaderSplashImageFacts((await loadSharp()) as any, body)\n if (!facts) return { error: 'That image could not be read.', status: 400 }\n // The bytes decide the type, never the name or the browser's claim.\n const problem = posReaderSplashImageProblem({ ...facts, bytes: body.length }, family)\n if (problem) return { error: problem, status: 400 }\n const sha256 = createHash('sha256').update(body).digest('hex')\n const extension = facts.contentType === 'image/png' ? 'png' : 'jpg'\n const storagePath = `platform/commerce/pos-reader-splash/${family.id}-${sha256.slice(0, 16)}.${extension}`\n await posReaderSplashBucket()\n .file(storagePath)\n .save(body, { contentType: facts.contentType, resumable: false })\n const previous = doc.families?.[family.id]\n if (previous?.storagePath && previous.storagePath !== storagePath) {\n await posReaderSplashBucket().file(previous.storagePath).delete().catch(() => undefined)\n }\n return {\n familyId: family.id,\n image: {\n storagePath,\n sha256,\n contentType: facts.contentType as PlatformSplashImage['contentType'],\n bytes: body.length,\n uploadedAtMs: Date.now(),\n uploadedBy: uid,\n },\n }\n}\n\n/** Every store with a Location, its screen brought up to the current default. */\nasync function applyToEveryStore() {\n const firestore = firebaseAdmin.app().firestore()\n const platform = await readPlatformSplash()\n const snapshot = await firestore.collectionGroup('terminal').limit(APPLY_WINDOW + 1).get()\n const hostIds = (snapshot.docs as Array<{ id: string; ref: { path: string }; get: (field: string) => unknown }>)\n .filter((doc) => doc.id === 'config' && doc.get('stripeLocationId'))\n .map((doc) => doc.ref.path.split('/')[1])\n .filter(Boolean)\n const counts = { applied: 0, unchanged: 0, failed: 0 }\n const queue = hostIds.slice(0, APPLY_WINDOW)\n await Promise.all(\n Array.from({ length: APPLY_CONCURRENCY }, async () => {\n for (let hostId = queue.shift(); hostId; hostId = queue.shift()) {\n const org = await ownerOrgOf(hostId)\n const result = await applyPosReaderSplash(hostId, { org, platform })\n if (result.outcome === 'applied' || result.outcome === 'cleared') counts.applied += 1\n else if (result.outcome === 'failed') counts.failed += 1\n else counts.unchanged += 1\n }\n }),\n )\n return { ...counts, more: snapshot.docs.length > APPLY_WINDOW }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse()\n if (!decoded['staff']) return json({ error: 'Staff only' }, 403)\n // A missing role claim reads as the least one, never as super.\n const role = String(decoded['staffRole'] ?? 'support')\n const firestore = firebaseAdmin.app().firestore()\n const doc = await readPlatformSplash()\n\n if (method === 'GET') {\n const preview = String(query?.['preview'] ?? '')\n if (preview) {\n const family = posReaderSplashFamily(preview)\n const image = family ? doc.families?.[family.id] : undefined\n if (!image) return json({ error: 'No image' }, 404)\n const [bytes] = (await posReaderSplashBucket().file(image.storagePath).download()) as [Buffer]\n return new Response(new Uint8Array(bytes), {\n status: 200,\n headers: { 'Content-Type': image.contentType, 'Cache-Control': 'no-store' },\n })\n }\n return json({ ...view(doc), canWrite: WRITE_ROLES.includes(role) })\n }\n\n if (method !== 'POST') return json({ error: 'Method not allowed' }, 405)\n if (!WRITE_ROLES.includes(role)) {\n return json({ error: 'Changing the reader start-up screen requires the super staff role.' }, 403)\n }\n const input = (body ?? {}) as Record<string, unknown>\n const action = String(input['action'] ?? '')\n const actor = { actorUid: decoded.uid, actorEmail: decoded.email ? String(decoded.email) : null }\n\n if (action === 'apply') return json({ ok: true, ...(await applyToEveryStore()) })\n\n if (action === 'upload' || action === 'remove') {\n let next: PlatformSplashDoc\n let familyId: string\n if (action === 'upload') {\n const outcome = await upload(doc, input, decoded.uid)\n if ('error' in outcome) return json({ error: outcome.error }, outcome.status)\n familyId = outcome.familyId\n next = { families: { ...(doc.families ?? {}), [familyId]: outcome.image }, updatedAtMs: Date.now() }\n } else {\n const family = posReaderSplashFamily(input['family'])\n if (!family) return json({ error: 'Unknown reader family' }, 400)\n familyId = family.id\n const families = { ...(doc.families ?? {}) }\n const previous = families[family.id]\n delete families[family.id]\n if (previous?.storagePath) {\n await posReaderSplashBucket().file(previous.storagePath).delete().catch(() => undefined)\n }\n next = { families, updatedAtMs: Date.now() }\n }\n await platformSplashRef().set(next)\n await addAdminAudit(firestore, {\n action: `pos-reader-splash-${action}`,\n ...actor,\n scope: 'posReaderSplash',\n target: `platformSettings/posReaderSplash`,\n before: { family: familyId, sha256: doc.families?.[familyId as keyof typeof doc.families]?.sha256 ?? null },\n after: { family: familyId, sha256: next.families?.[familyId as keyof typeof next.families]?.sha256 ?? null },\n at: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return json({ ok: true, ...view(next), canWrite: true })\n }\n return json({ error: 'Unknown action' }, 400)\n } catch (error) {\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[pos-reader-splash-admin]', error)\n return json({ error: 'The reader start-up screen could not be updated' }, 500)\n }\n}\n\nexport { handler as posReaderSplashAdmin }\n"],"names":["createHash","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","invalidIdTokenResponse","POS_READER_SPLASH_FAMILIES","POS_READER_SPLASH_MAX_BYTES","posReaderSplashFamily","posReaderSplashImageProblem","applyPosReaderSplash","ownerOrgOf","platformSplashReady","platformSplashRef","posReaderSplashBucket","readPlatformSplash","posReaderSplashImageFacts","WRITE_ROLES","APPLY_WINDOW","APPLY_CONCURRENCY","json","body","status","Response","headers","view","doc","ready","families","map","family","image","id","label","width","height","contentType","bytes","sha256","uploadedAtMs","updatedAtMs","upload","input","uid","error","raw","String","length","Math","ceil","Buffer","from","loadSharp","facts","problem","update","digest","extension","storagePath","slice","file","save","resumable","previous","delete","catch","undefined","familyId","Date","now","uploadedBy","applyToEveryStore","firestore","app","platform","snapshot","collectionGroup","limit","get","hostIds","docs","filter","ref","path","split","Boolean","counts","applied","unchanged","failed","queue","Promise","all","Array","hostId","shift","org","result","outcome","more","handler","request","method","query","rawHeaders","authorization","idToken","startsWith","decoded","auth","verifyIdToken","email_verified","role","preview","download","Uint8Array","canWrite","includes","action","actor","actorUid","actorEmail","email","ok","next","set","scope","target","before","after","at","FieldValue","serverTimestamp","unauthenticated","console","posReaderSplashAdmin"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { Sharp, SharpOptions } from 'sharp';
|
|
18
|
+
import { type PosReaderSplashFamily } from '../model/pos-reader-splash';
|
|
19
|
+
type SharpFactory = (input?: Buffer | SharpOptions, options?: SharpOptions) => Sharp;
|
|
20
|
+
export type PosReaderSplashRenderMode = 'artwork' | 'logo';
|
|
21
|
+
export interface RenderedPosReaderSplash {
|
|
22
|
+
body: Buffer;
|
|
23
|
+
contentType: 'image/png' | 'image/jpeg';
|
|
24
|
+
}
|
|
25
|
+
export declare class PosReaderSplashRenderError extends Error {
|
|
26
|
+
}
|
|
27
|
+
/** One reader family's start-up screen from `source`. */
|
|
28
|
+
export declare function renderPosReaderSplash(options: {
|
|
29
|
+
source: Buffer;
|
|
30
|
+
family: PosReaderSplashFamily;
|
|
31
|
+
mode: PosReaderSplashRenderMode;
|
|
32
|
+
sharp: SharpFactory;
|
|
33
|
+
}): Promise<RenderedPosReaderSplash>;
|
|
34
|
+
/** An image's format and size, read without decoding its pixels. */
|
|
35
|
+
export declare function posReaderSplashImageFacts(sharp: SharpFactory, body: Buffer): Promise<{
|
|
36
|
+
contentType: string;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
} | null>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { POS_READER_SPLASH_MAX_BYTES, posReaderSplashImageProblem } from "../model/pos-reader-splash.js";
|
|
18
|
+
/** A source whose decoded pixels exceed this is refused rather than decoded. */ const MAX_INPUT_PIXELS = 50000000;
|
|
19
|
+
/** The logo's box, as a share of the screen. */ const LOGO_BOX = {
|
|
20
|
+
width: 0.6,
|
|
21
|
+
height: 0.3
|
|
22
|
+
};
|
|
23
|
+
/** The opaque ground a logo sits on. */ const LOGO_GROUND = {
|
|
24
|
+
r: 255,
|
|
25
|
+
g: 255,
|
|
26
|
+
b: 255,
|
|
27
|
+
alpha: 1
|
|
28
|
+
};
|
|
29
|
+
/** JPEG qualities tried, best first, when a PNG is too large for Stripe. */ const JPEG_QUALITIES = [
|
|
30
|
+
90,
|
|
31
|
+
82,
|
|
32
|
+
74,
|
|
33
|
+
66
|
|
34
|
+
];
|
|
35
|
+
export class PosReaderSplashRenderError extends Error {
|
|
36
|
+
}
|
|
37
|
+
/** Decoding options; an SVG is rasterized at a density sharp at `target`. */ async function decodeOptions(sharp, source, target) {
|
|
38
|
+
var _meta_width, _meta_height;
|
|
39
|
+
const base = {
|
|
40
|
+
limitInputPixels: MAX_INPUT_PIXELS,
|
|
41
|
+
failOn: 'error'
|
|
42
|
+
};
|
|
43
|
+
const meta = await sharp(source, base).metadata();
|
|
44
|
+
if (meta.format !== 'svg') return base;
|
|
45
|
+
const longest = Math.max((_meta_width = meta.width) != null ? _meta_width : 0, (_meta_height = meta.height) != null ? _meta_height : 0);
|
|
46
|
+
if (!longest) return base;
|
|
47
|
+
return _extends({}, base, {
|
|
48
|
+
density: Math.min(2400, Math.max(1, Math.ceil(72 * target / longest)))
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function drawn(sharp, source, family, mode) {
|
|
52
|
+
if (mode === 'artwork') {
|
|
53
|
+
const options = await decodeOptions(sharp, source, family.height);
|
|
54
|
+
return sharp(source, options).rotate().resize(family.width, family.height, {
|
|
55
|
+
fit: 'cover'
|
|
56
|
+
}).flatten({
|
|
57
|
+
background: LOGO_GROUND
|
|
58
|
+
}).png().toBuffer();
|
|
59
|
+
}
|
|
60
|
+
const boxWidth = Math.round(family.width * LOGO_BOX.width);
|
|
61
|
+
const boxHeight = Math.round(family.height * LOGO_BOX.height);
|
|
62
|
+
const options = await decodeOptions(sharp, source, Math.max(boxWidth, boxHeight));
|
|
63
|
+
const mark = await sharp(source, options).rotate().resize(boxWidth, boxHeight, {
|
|
64
|
+
fit: 'inside'
|
|
65
|
+
}).png().toBuffer();
|
|
66
|
+
const composed = await sharp({
|
|
67
|
+
create: {
|
|
68
|
+
width: family.width,
|
|
69
|
+
height: family.height,
|
|
70
|
+
channels: 4,
|
|
71
|
+
background: LOGO_GROUND
|
|
72
|
+
}
|
|
73
|
+
}).composite([
|
|
74
|
+
{
|
|
75
|
+
input: mark,
|
|
76
|
+
gravity: 'center'
|
|
77
|
+
}
|
|
78
|
+
]).png().toBuffer();
|
|
79
|
+
// A second pass: sharp flattens before it composites within one pipeline,
|
|
80
|
+
// and the screen has no use for an alpha channel.
|
|
81
|
+
return sharp(composed).flatten({
|
|
82
|
+
background: LOGO_GROUND
|
|
83
|
+
}).png().toBuffer();
|
|
84
|
+
}
|
|
85
|
+
/** One reader family's start-up screen from `source`. */ export async function renderPosReaderSplash(options) {
|
|
86
|
+
var _meta_width, _meta_height;
|
|
87
|
+
const { source, family, mode, sharp } = options;
|
|
88
|
+
let png;
|
|
89
|
+
try {
|
|
90
|
+
png = await drawn(sharp, source, family, mode);
|
|
91
|
+
} catch (unused) {
|
|
92
|
+
throw new PosReaderSplashRenderError('That image could not be read.');
|
|
93
|
+
}
|
|
94
|
+
let rendered = {
|
|
95
|
+
body: png,
|
|
96
|
+
contentType: 'image/png'
|
|
97
|
+
};
|
|
98
|
+
if (png.length >= POS_READER_SPLASH_MAX_BYTES) {
|
|
99
|
+
for (const quality of JPEG_QUALITIES){
|
|
100
|
+
const body = await sharp(png).jpeg({
|
|
101
|
+
quality,
|
|
102
|
+
mozjpeg: true
|
|
103
|
+
}).toBuffer();
|
|
104
|
+
rendered = {
|
|
105
|
+
body,
|
|
106
|
+
contentType: 'image/jpeg'
|
|
107
|
+
};
|
|
108
|
+
if (body.length < POS_READER_SPLASH_MAX_BYTES) break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const meta = await sharp(rendered.body).metadata();
|
|
112
|
+
const problem = posReaderSplashImageProblem({
|
|
113
|
+
contentType: rendered.contentType,
|
|
114
|
+
bytes: rendered.body.length,
|
|
115
|
+
width: (_meta_width = meta.width) != null ? _meta_width : 0,
|
|
116
|
+
height: (_meta_height = meta.height) != null ? _meta_height : 0
|
|
117
|
+
}, family);
|
|
118
|
+
if (problem) throw new PosReaderSplashRenderError(problem);
|
|
119
|
+
return rendered;
|
|
120
|
+
}
|
|
121
|
+
/** An image's format and size, read without decoding its pixels. */ export async function posReaderSplashImageFacts(sharp, body) {
|
|
122
|
+
try {
|
|
123
|
+
var _meta_width, _meta_height;
|
|
124
|
+
const meta = await sharp(body, {
|
|
125
|
+
limitInputPixels: MAX_INPUT_PIXELS
|
|
126
|
+
}).metadata();
|
|
127
|
+
const contentType = meta.format === 'png' ? 'image/png' : meta.format === 'jpeg' ? 'image/jpeg' : meta.format === 'webp' ? 'image/webp' : '';
|
|
128
|
+
return {
|
|
129
|
+
contentType,
|
|
130
|
+
width: (_meta_width = meta.width) != null ? _meta_width : 0,
|
|
131
|
+
height: (_meta_height = meta.height) != null ? _meta_height : 0
|
|
132
|
+
};
|
|
133
|
+
} catch (unused) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=pos-reader-splash-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-reader-splash-render.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Sharp, SharpOptions } from 'sharp'\nimport {\n POS_READER_SPLASH_MAX_BYTES,\n posReaderSplashImageProblem,\n type PosReaderSplashFamily,\n} from '../model/pos-reader-splash'\n\n/*==========================================\n * Draws a reader's start-up screen (AGL-4048) at the exact size Stripe asks\n * for, from either:\n *\n * - `artwork` a full-screen portrait image, scaled to cover the screen\n * (a 9:16 image covers both readers' screens whole);\n * - `logo` a logo, fitted inside the middle of the screen on an opaque\n * white ground, never stretched.\n *\n * The logo's box is 60% of the width and 30% of the height: on a 1080-wide\n * portrait screen a square logo comes out 648 px, and a wide wordmark keeps\n * a margin either side. White is the ground the approved workspace-logo\n * standard sets (a square logo on an opaque white plate), so the same logo\n * reads the same on a reader as in the apps.\n *\n * The result is PNG, the lossless choice for flat artwork; a photo that\n * comes out at 2 MB or more is re-encoded as JPEG until it fits, and every\n * screen is checked against Stripe's rules before it is handed back.\n *=========================================*/\n\ntype SharpFactory = (input?: Buffer | SharpOptions, options?: SharpOptions) => Sharp\n\nexport type PosReaderSplashRenderMode = 'artwork' | 'logo'\n\n/** A source whose decoded pixels exceed this is refused rather than decoded. */\nconst MAX_INPUT_PIXELS = 50_000_000\n\n/** The logo's box, as a share of the screen. */\nconst LOGO_BOX = { width: 0.6, height: 0.3 } as const\n\n/** The opaque ground a logo sits on. */\nconst LOGO_GROUND = { r: 255, g: 255, b: 255, alpha: 1 } as const\n\n/** JPEG qualities tried, best first, when a PNG is too large for Stripe. */\nconst JPEG_QUALITIES = [90, 82, 74, 66] as const\n\nexport interface RenderedPosReaderSplash {\n body: Buffer\n contentType: 'image/png' | 'image/jpeg'\n}\n\nexport class PosReaderSplashRenderError extends Error {}\n\n/** Decoding options; an SVG is rasterized at a density sharp at `target`. */\nasync function decodeOptions(sharp: SharpFactory, source: Buffer, target: number): Promise<SharpOptions> {\n const base: SharpOptions = { limitInputPixels: MAX_INPUT_PIXELS, failOn: 'error' }\n const meta = await sharp(source, base).metadata()\n if (meta.format !== 'svg') return base\n const longest = Math.max(meta.width ?? 0, meta.height ?? 0)\n if (!longest) return base\n return { ...base, density: Math.min(2400, Math.max(1, Math.ceil((72 * target) / longest))) }\n}\n\nasync function drawn(\n sharp: SharpFactory,\n source: Buffer,\n family: PosReaderSplashFamily,\n mode: PosReaderSplashRenderMode,\n): Promise<Buffer> {\n if (mode === 'artwork') {\n const options = await decodeOptions(sharp, source, family.height)\n return sharp(source, options)\n .rotate()\n .resize(family.width, family.height, { fit: 'cover' })\n .flatten({ background: LOGO_GROUND })\n .png()\n .toBuffer()\n }\n const boxWidth = Math.round(family.width * LOGO_BOX.width)\n const boxHeight = Math.round(family.height * LOGO_BOX.height)\n const options = await decodeOptions(sharp, source, Math.max(boxWidth, boxHeight))\n const mark = await sharp(source, options)\n .rotate()\n .resize(boxWidth, boxHeight, { fit: 'inside' })\n .png()\n .toBuffer()\n const composed = await sharp({\n create: { width: family.width, height: family.height, channels: 4, background: LOGO_GROUND },\n })\n .composite([{ input: mark, gravity: 'center' }])\n .png()\n .toBuffer()\n // A second pass: sharp flattens before it composites within one pipeline,\n // and the screen has no use for an alpha channel.\n return sharp(composed).flatten({ background: LOGO_GROUND }).png().toBuffer()\n}\n\n/** One reader family's start-up screen from `source`. */\nexport async function renderPosReaderSplash(options: {\n source: Buffer\n family: PosReaderSplashFamily\n mode: PosReaderSplashRenderMode\n sharp: SharpFactory\n}): Promise<RenderedPosReaderSplash> {\n const { source, family, mode, sharp } = options\n let png: Buffer\n try {\n png = await drawn(sharp, source, family, mode)\n } catch {\n throw new PosReaderSplashRenderError('That image could not be read.')\n }\n let rendered: RenderedPosReaderSplash = { body: png, contentType: 'image/png' }\n if (png.length >= POS_READER_SPLASH_MAX_BYTES) {\n for (const quality of JPEG_QUALITIES) {\n const body = await sharp(png).jpeg({ quality, mozjpeg: true }).toBuffer()\n rendered = { body, contentType: 'image/jpeg' }\n if (body.length < POS_READER_SPLASH_MAX_BYTES) break\n }\n }\n const meta = await sharp(rendered.body).metadata()\n const problem = posReaderSplashImageProblem(\n {\n contentType: rendered.contentType,\n bytes: rendered.body.length,\n width: meta.width ?? 0,\n height: meta.height ?? 0,\n },\n family,\n )\n if (problem) throw new PosReaderSplashRenderError(problem)\n return rendered\n}\n\n/** An image's format and size, read without decoding its pixels. */\nexport async function posReaderSplashImageFacts(\n sharp: SharpFactory,\n body: Buffer,\n): Promise<{ contentType: string; width: number; height: number } | null> {\n try {\n const meta = await sharp(body, { limitInputPixels: MAX_INPUT_PIXELS }).metadata()\n const contentType =\n meta.format === 'png' ? 'image/png' : meta.format === 'jpeg' ? 'image/jpeg' : meta.format === 'webp' ? 'image/webp' : ''\n return { contentType, width: meta.width ?? 0, height: meta.height ?? 0 }\n } catch {\n return null\n }\n}\n"],"names":["POS_READER_SPLASH_MAX_BYTES","posReaderSplashImageProblem","MAX_INPUT_PIXELS","LOGO_BOX","width","height","LOGO_GROUND","r","g","b","alpha","JPEG_QUALITIES","PosReaderSplashRenderError","Error","decodeOptions","sharp","source","target","meta","base","limitInputPixels","failOn","metadata","format","longest","Math","max","density","min","ceil","drawn","family","mode","options","rotate","resize","fit","flatten","background","png","toBuffer","boxWidth","round","boxHeight","mark","composed","create","channels","composite","input","gravity","renderPosReaderSplash","rendered","body","contentType","length","quality","jpeg","mozjpeg","problem","bytes","posReaderSplashImageFacts"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The server helpers the card reader start-up screen (AGL-4048) loads only
|
|
19
|
+
* when it draws or reads an image: the media CDN's block, a media asset's
|
|
20
|
+
* storage path, `sharp`, and the pinned fetch for an outside logo address.
|
|
21
|
+
*
|
|
22
|
+
* Loaded by RELATIVE path from `pos-reader-splash.ts` and
|
|
23
|
+
* `pos-reader-splash-admin.ts`, never by importing `@aglyn/tenant-data-admin`
|
|
24
|
+
* lazily: a workspace lib `import()`ed by its package specifier is lazy-loaded
|
|
25
|
+
* everywhere as far as nx is concerned, and every static import of it across
|
|
26
|
+
* this plugin would then fail the module boundary rule
|
|
27
|
+
* (`ai-media-asset-server.ts` in the AI plugin is the same arrangement).
|
|
28
|
+
*/
|
|
29
|
+
export { mediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path';
|
|
30
|
+
export { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants';
|
|
31
|
+
export { mediaCdnServeBlock } from '@aglyn/tenant-data-admin/server/serve-media-cdn';
|
|
32
|
+
export { createPinnedDispatcher, resolvePublicIp } from '@aglyn/tenant-data-admin/server/serve-plugin-fetch';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The server helpers the card reader start-up screen (AGL-4048) loads only
|
|
18
|
+
* when it draws or reads an image: the media CDN's block, a media asset's
|
|
19
|
+
* storage path, `sharp`, and the pinned fetch for an outside logo address.
|
|
20
|
+
*
|
|
21
|
+
* Loaded by RELATIVE path from `pos-reader-splash.ts` and
|
|
22
|
+
* `pos-reader-splash-admin.ts`, never by importing `@aglyn/tenant-data-admin`
|
|
23
|
+
* lazily: a workspace lib `import()`ed by its package specifier is lazy-loaded
|
|
24
|
+
* everywhere as far as nx is concerned, and every static import of it across
|
|
25
|
+
* this plugin would then fail the module boundary rule
|
|
26
|
+
* (`ai-media-asset-server.ts` in the AI plugin is the same arrangement).
|
|
27
|
+
*/ export { mediaStoragePathInScope } from "@aglyn/tenant-data-admin/server/media-storage-path";
|
|
28
|
+
export { loadSharp } from "@aglyn/tenant-data-admin/server/media-variants";
|
|
29
|
+
export { mediaCdnServeBlock } from "@aglyn/tenant-data-admin/server/serve-media-cdn";
|
|
30
|
+
export { createPinnedDispatcher, resolvePublicIp } from "@aglyn/tenant-data-admin/server/serve-plugin-fetch";
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=pos-reader-splash-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-reader-splash-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The server helpers the card reader start-up screen (AGL-4048) loads only\n * when it draws or reads an image: the media CDN's block, a media asset's\n * storage path, `sharp`, and the pinned fetch for an outside logo address.\n *\n * Loaded by RELATIVE path from `pos-reader-splash.ts` and\n * `pos-reader-splash-admin.ts`, never by importing `@aglyn/tenant-data-admin`\n * lazily: a workspace lib `import()`ed by its package specifier is lazy-loaded\n * everywhere as far as nx is concerned, and every static import of it across\n * this plugin would then fail the module boundary rule\n * (`ai-media-asset-server.ts` in the AI plugin is the same arrangement).\n */\nexport { mediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path'\nexport { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants'\nexport { mediaCdnServeBlock } from '@aglyn/tenant-data-admin/server/serve-media-cdn'\nexport { createPinnedDispatcher, resolvePublicIp } from '@aglyn/tenant-data-admin/server/serve-plugin-fetch'\n"],"names":["mediaStoragePathInScope","loadSharp","mediaCdnServeBlock","createPinnedDispatcher","resolvePublicIp"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GACD,SAASA,uBAAuB,QAAQ,qDAAoD;AAC5F,SAASC,SAAS,QAAQ,iDAAgD;AAC1E,SAASC,kBAAkB,QAAQ,kDAAiD;AACpF,SAASC,sBAAsB,EAAEC,eAAe,QAAQ,qDAAoD"}
|