your-ai-workflow-firebase-os 1.2.60 → 1.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.
@@ -0,0 +1,21 @@
1
+ import { Request, Router } from 'express';
2
+ import { default as Stripe } from 'stripe';
3
+ import { VerifiedUser } from './auth';
4
+ export interface ApiRoutesOptions {
5
+ /**
6
+ * Called for every Stripe event that arrives at the webhook, after its
7
+ * signature is verified. This is where an app reacts to something that
8
+ * happened outside it — email a receipt, notify the owner of a refund.
9
+ * Reading payment state needs nothing here; the routes read Stripe live.
10
+ */
11
+ onStripeEvent?: (event: Stripe.Event) => void | Promise<void>;
12
+ }
13
+ export interface AuthedRequest extends Request {
14
+ user?: VerifiedUser;
15
+ }
16
+ export declare function apiRoutes(options?: ApiRoutesOptions): Router;
17
+ export { projectId, verifyIdToken } from './auth';
18
+ export { runSelfTest, selfTestAllowed } from './selftest';
19
+ export { checkoutProblem, grantsAccess, returnUrls, stripeMode, MAX_ADHOC_AMOUNT, MIN_ADHOC_AMOUNT, } from './stripeClient';
20
+ export type { CheckoutRequest } from './stripeClient';
21
+ export type { Check, SelfTestResult } from './selftest';
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunk-DYteiM-1.cjs`);let t=require(`express`);t=e.i(t,1);let n=require(`node:crypto`),r=require(`stripe`);r=e.i(r,1);var i=`https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com`,a=60;function o(e,t,n,r=Math.floor(Date.now()/1e3)){return n?e.alg===`RS256`?e.kid?t.aud===n?t.iss===`https://securetoken.google.com/${n}`?t.sub?t.sub.length>128?`Token subject is not a valid uid.`:typeof t.exp!=`number`||t.exp<=r?`Token has expired.`:typeof t.iat!=`number`||t.iat>r+a?`Token was issued in the future.`:null:`Token has no subject.`:`Token has the wrong issuer.`:`Token was issued for a different Firebase project.`:`Token has no key id.`:`Token is not signed with RS256.`:`The server does not know its Firebase project id.`}function s(e){return Buffer.from(e.replace(/-/g,`+`).replace(/_/g,`/`),`base64`)}function c(e){let t=String(e||``).split(`.`);if(t.length!==3)return null;try{return{header:JSON.parse(s(t[0]).toString(`utf8`)),claims:JSON.parse(s(t[1]).toString(`utf8`)),signed:`${t[0]}.${t[1]}`,signature:s(t[2])}}catch{return null}}var l=null;async function u(){if(l&&l.expiresAt>Date.now())return l.keys;let e=await fetch(i);if(!e.ok)throw Error(`Could not fetch Google signing keys (${e.status}).`);let t=await e.json(),n=/max-age=(\d+)/.exec(e.headers.get(`cache-control`)||``);return l={keys:t,expiresAt:Date.now()+(n?Number(n[1]):3600)*1e3},t}async function d(e,t){let r=c(e);if(!r)throw Error(`That is not a valid sign-in token.`);let i=o(r.header,r.claims,t);if(i)throw Error(i);let a=(await u())[r.header.kid];if(!a)throw Error(`Token was signed with a key Google no longer publishes.`);let s=(0,n.createVerify)(`RSA-SHA256`);if(s.update(r.signed),s.end(),!s.verify(a,r.signature))throw Error(`Token signature does not check out.`);return{uid:r.claims.sub,email:r.claims.email||null,emailVerified:!!r.claims.email_verified}}function f(){return(process.env.VITE_FIREBASE_PROJECT_ID||process.env.FIREBASE_PROJECT_ID||``).trim()}var p=null;function m(){return(process.env.STRIPE_SECRET_KEY||``).trim()}function h(){let e=m();return e?e.startsWith(`sk_live_`)?`live`:`test`:null}function g(){let e=m();if(!e)throw Error(`STRIPE_SECRET_KEY is not set.`);return p||=new r.default(e),p}var _=Number(process.env.STRIPE_MAX_ADHOC_AMOUNT||1e4),v=.5;function y(e){let{priceId:t,amount:n,mode:r=`payment`,quantity:i=1}=e||{};if(!t&&n===void 0)return`This button does not say what is being sold.`;if(t&&n!==void 0)return`A payment has either a Stripe price or an amount, not both.`;if(r!==`payment`&&r!==`subscription`)return`A payment is either one-time or a subscription.`;if(!Number.isInteger(i)||i<1||i>999)return`Quantity must be a whole number between 1 and 999.`;if(n!==void 0){if(r===`subscription`)return`A subscription needs a price created in Stripe — a typed amount cannot recur.`;if(typeof n!=`number`||!Number.isFinite(n))return`That amount is not a number.`;if(n<.5)return`The smallest amount that can be charged is ${v}.`;if(n>_)return`That amount is above the ${_} limit for typed prices. Use a price created in Stripe.`;if(!e.productName||!String(e.productName).trim())return`A typed amount needs a name for what is being bought.`}return t&&!/^price_[A-Za-z0-9]+$/.test(t)?`That does not look like a Stripe price id.`:null}function b(e){return Math.round(e*100)}function x(e,t,n){let r=String(e||``).replace(/\/+$/,``),i=(e,t)=>{let n=String(e||t);return n.startsWith(`/`)?n:`/${n}`};return{success:`${r}${i(t,`/payment-complete`)}?session_id={CHECKOUT_SESSION_ID}`,cancel:`${r}${i(n,`/payment-cancelled`)}`}}function S(e){return e===`active`||e===`trialing`||e===`past_due`}var C=e=>!!(process.env[e]||``).trim();function w(e){let t=(process.env.SELFTEST_TOKEN||``).trim();return t?String(e||``)===t:!0}async function T(){let e=[{name:`Server`,status:`pass`,detail:`The backend is running and reachable.`}],t=f();if(!t)e.push({name:`Firebase project`,status:`fail`,detail:`VITE_FIREBASE_PROJECT_ID is not set, so the server cannot tell who is signed in.`});else{e.push({name:`Firebase project`,status:`pass`,detail:`Signing in against ${t}.`});try{let t=await u();e.push({name:`Sign-in verification`,status:`pass`,detail:`Google's signing keys are reachable (${Object.keys(t).length} in rotation).`})}catch(t){e.push({name:`Sign-in verification`,status:`fail`,detail:`Could not reach Google's signing keys: ${t.message}`})}}if(!m())e.push({name:`Stripe`,status:`warn`,detail:`STRIPE_SECRET_KEY is not set. Payments are switched off until it is.`});else{let t=h();try{let n=await g().accounts.retrieve();e.push({name:`Stripe`,status:`pass`,detail:`Connected in ${t} mode to ${n.business_profile?.name||n.id}.`}),t===`live`&&e.push({name:`Live mode`,status:`warn`,detail:`This app is charging real cards. Every payment made here is real money.`})}catch(t){e.push({name:`Stripe`,status:`fail`,detail:`The key is set but Stripe refused it: ${t.message}`})}e.push(C(`STRIPE_WEBHOOK_SECRET`)?{name:`Stripe webhook`,status:`pass`,detail:`Signed webhooks will be accepted.`}:{name:`Stripe webhook`,status:`warn`,detail:`STRIPE_WEBHOOK_SECRET is not set. Payments still confirm when the payer returns; refunds and cancellations made in the Stripe dashboard will not reach the app until it is.`})}return e.push(C(`RESEND_API_KEY`)?{name:`Email`,status:`pass`,detail:`RESEND_API_KEY is set.`}:{name:`Email`,status:`warn`,detail:`RESEND_API_KEY is not set. The app will not send its own email.`}),{ok:!e.some(e=>e.status===`fail`),service:`your-ai-workflow-firebase-os`,checks:e}}var E=(e,t,n,r={})=>e.status(t).json({success:!1,error:n,...r});function D(e,t){let n=t instanceof Error?t.message:`Something went wrong on the server.`,r=n.includes(`STRIPE_SECRET_KEY`)||n.includes(`project id`);return E(e,r?503:500,n,r?{requiresSetup:!0}:{})}async function O(e,t,n){let r=String(e.headers.authorization||``),i=r.startsWith(`Bearer `)?r.slice(7):``;if(!i)return E(t,401,`Please sign in first.`);try{e.user=await d(i,f()),n()}catch(e){return E(t,401,e.message)}}async function k(e,t){return!t.email||!t.emailVerified?null:(await e.customers.list({email:t.email,limit:1})).data[0]||null}async function A(e,t){let n=await k(e,t);return n?(n.metadata?.uid!==t.uid&&await e.customers.update(n.id,{metadata:{...n.metadata,uid:t.uid}}),n):e.customers.create({email:t.email||void 0,metadata:{uid:t.uid}})}function j(e){return e?.current_period_end??e?.items?.data?.[0]?.current_period_end??null}function M(e){let t=e.items?.data?.[0],n=t?.price;return{id:e.id,status:e.status,active:S(e.status),priceId:n?.id||null,productName:typeof n?.product==`object`&&n.product?.name||null,amount:n?.unit_amount==null?null:n.unit_amount/100,currency:n?.currency||null,interval:n?.recurring?.interval||null,quantity:t?.quantity??1,currentPeriodEnd:j(e),cancelAtPeriodEnd:!!e.cancel_at_period_end}}function N(e){return{id:e.id,amount:e.amount/100,currency:e.currency,created:e.created,description:e.description||null,paid:e.paid&&e.status===`succeeded`,refunded:e.refunded,amountRefunded:e.amount_refunded/100,fullyRefunded:e.refunded&&e.amount_refunded>=e.amount}}var P=new Set;function F(e){return P.has(e)?!0:(P.add(e),P.size>500&&P.delete(P.values().next().value),!1)}function I(e={}){let n=t.default.Router();return n.post(`/payments/webhook`,t.default.raw({type:`application/json`}),async(t,n)=>{let r=(process.env.STRIPE_WEBHOOK_SECRET||``).trim(),i=t.headers[`stripe-signature`];if(!r)return E(n,503,`STRIPE_WEBHOOK_SECRET is not set.`);if(!i)return E(n,400,`Missing stripe-signature header.`);let a;try{a=g().webhooks.constructEvent(t.body,i,r)}catch(e){return E(n,400,`Webhook signature check failed: ${e.message}`)}if(F(a.id))return n.json({received:!0,duplicate:!0});try{await e.onStripeEvent?.(a)}catch(e){return P.delete(a.id),E(n,503,`Handler failed: ${e.message}`)}return n.json({received:!0})}),n.use(t.default.json()),n.get(`/_selftest`,async(e,t)=>{if(!w(e.query.token))return E(t,403,`This check needs its token.`);try{let e=await T();return t.status(e.ok?200:500).json(e)}catch(e){return D(t,e)}}),n.get(`/payments/status`,(e,t)=>{let n=h();t.json({configured:!!m(),mode:n,webhookConfigured:!!(process.env.STRIPE_WEBHOOK_SECRET||``).trim(),message:n?`Stripe is connected in ${n} mode.`:`Add STRIPE_SECRET_KEY to start taking payments.`})}),n.post(`/payments/checkout`,async(e,t)=>{let n=e.body||{},r=y(n);if(r)return E(t,400,r);let i=n.origin||e.headers.origin;if(!i)return E(t,400,`The page did not say where to send the payer back to.`);try{let r=g(),a=null,o=String(e.headers.authorization||``);if(o.startsWith(`Bearer `))try{a=await d(o.slice(7),f())}catch{a=null}let s=a?await A(r,a):null,c=x(String(i),n.successPath,n.cancelPath),l=n.priceId?{price:n.priceId,quantity:n.quantity||1}:{quantity:n.quantity||1,price_data:{currency:n.currency||`usd`,product_data:{name:String(n.productName)},unit_amount:b(Number(n.amount))}},u=await r.checkout.sessions.create({mode:n.mode||`payment`,line_items:[l],success_url:c.success,cancel_url:c.cancel,customer:s?.id,allow_promotion_codes:!0,metadata:{...n.metadata||{},...a?{uid:a.uid}:{}}});return t.json({success:!0,sessionId:u.id,url:u.url})}catch(e){return D(t,e)}}),n.get(`/payments/session/:id`,async(e,t)=>{try{let n=await g().checkout.sessions.retrieve(String(e.params.id));return t.json({success:!0,paid:n.payment_status===`paid`,paymentStatus:n.payment_status,mode:n.mode,amountTotal:n.amount_total==null?null:n.amount_total/100,currency:n.currency,customerEmail:n.customer_details?.email||null,customerName:n.customer_details?.name||null,metadata:n.metadata||{}})}catch(e){return(e instanceof Error?e.message:`Unknown error`).includes(`STRIPE_SECRET_KEY`)?D(t,e):E(t,404,`That payment could not be found.`)}}),n.get(`/payments/me`,O,async(e,t)=>{try{let n=g(),r=await k(n,e.user);if(!r)return t.json({success:!0,customerId:null,active:!1,subscriptions:[],payments:[]});let[i,a]=await Promise.all([n.subscriptions.list({customer:r.id,status:`all`,limit:10,expand:[`data.items.data.price.product`]}),n.charges.list({customer:r.id,limit:20})]),o=i.data.map(M),s=a.data.map(N);return t.json({success:!0,customerId:r.id,email:r.email,active:o.some(e=>e.active)||s.some(e=>e.paid&&!e.fullyRefunded),subscriptions:o,payments:s})}catch(e){return D(t,e)}}),n.post(`/payments/portal`,O,async(e,t)=>{try{let n=g(),r=await k(n,e.user);if(!r)return E(t,404,`There is nothing to manage on this account yet.`);let i=e.body?.origin||e.headers.origin;if(!i)return E(t,400,`The page did not say where to send them back to.`);let a=await n.billingPortal.sessions.create({customer:r.id,return_url:String(i).replace(/\/+$/,``)});return t.json({success:!0,url:a.url})}catch(e){return D(t,e)}}),n}exports.MAX_ADHOC_AMOUNT=_,exports.MIN_ADHOC_AMOUNT=v,exports.apiRoutes=I,exports.checkoutProblem=y,exports.grantsAccess=S,exports.projectId=f,exports.returnUrls=x,exports.runSelfTest=T,exports.selfTestAllowed=w,exports.stripeMode=h,exports.verifyIdToken=d;
@@ -0,0 +1,409 @@
1
+ import e from "express";
2
+ import { createVerify as t } from "node:crypto";
3
+ import n from "stripe";
4
+ //#region src/server/auth.ts
5
+ var r = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com", i = 60;
6
+ function a(e, t, n, r = Math.floor(Date.now() / 1e3)) {
7
+ return n ? e.alg === "RS256" ? e.kid ? t.aud === n ? t.iss === `https://securetoken.google.com/${n}` ? t.sub ? t.sub.length > 128 ? "Token subject is not a valid uid." : typeof t.exp != "number" || t.exp <= r ? "Token has expired." : typeof t.iat != "number" || t.iat > r + i ? "Token was issued in the future." : null : "Token has no subject." : "Token has the wrong issuer." : "Token was issued for a different Firebase project." : "Token has no key id." : "Token is not signed with RS256." : "The server does not know its Firebase project id.";
8
+ }
9
+ function o(e) {
10
+ return Buffer.from(e.replace(/-/g, "+").replace(/_/g, "/"), "base64");
11
+ }
12
+ function s(e) {
13
+ let t = String(e || "").split(".");
14
+ if (t.length !== 3) return null;
15
+ try {
16
+ return {
17
+ header: JSON.parse(o(t[0]).toString("utf8")),
18
+ claims: JSON.parse(o(t[1]).toString("utf8")),
19
+ signed: `${t[0]}.${t[1]}`,
20
+ signature: o(t[2])
21
+ };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ var c = null;
27
+ async function l() {
28
+ if (c && c.expiresAt > Date.now()) return c.keys;
29
+ let e = await fetch(r);
30
+ if (!e.ok) throw Error(`Could not fetch Google signing keys (${e.status}).`);
31
+ let t = await e.json(), n = /max-age=(\d+)/.exec(e.headers.get("cache-control") || "");
32
+ return c = {
33
+ keys: t,
34
+ expiresAt: Date.now() + (n ? Number(n[1]) : 3600) * 1e3
35
+ }, t;
36
+ }
37
+ async function u(e, n) {
38
+ let r = s(e);
39
+ if (!r) throw Error("That is not a valid sign-in token.");
40
+ let i = a(r.header, r.claims, n);
41
+ if (i) throw Error(i);
42
+ let o = (await l())[r.header.kid];
43
+ if (!o) throw Error("Token was signed with a key Google no longer publishes.");
44
+ let c = t("RSA-SHA256");
45
+ if (c.update(r.signed), c.end(), !c.verify(o, r.signature)) throw Error("Token signature does not check out.");
46
+ return {
47
+ uid: r.claims.sub,
48
+ email: r.claims.email || null,
49
+ emailVerified: !!r.claims.email_verified
50
+ };
51
+ }
52
+ function d() {
53
+ return (process.env.VITE_FIREBASE_PROJECT_ID || process.env.FIREBASE_PROJECT_ID || "").trim();
54
+ }
55
+ //#endregion
56
+ //#region src/server/stripeClient.ts
57
+ var f = null;
58
+ function p() {
59
+ return (process.env.STRIPE_SECRET_KEY || "").trim();
60
+ }
61
+ function m() {
62
+ let e = p();
63
+ return e ? e.startsWith("sk_live_") ? "live" : "test" : null;
64
+ }
65
+ function h() {
66
+ let e = p();
67
+ if (!e) throw Error("STRIPE_SECRET_KEY is not set.");
68
+ return f ||= new n(e), f;
69
+ }
70
+ var g = Number(process.env.STRIPE_MAX_ADHOC_AMOUNT || 1e4), _ = .5;
71
+ function v(e) {
72
+ let { priceId: t, amount: n, mode: r = "payment", quantity: i = 1 } = e || {};
73
+ if (!t && n === void 0) return "This button does not say what is being sold.";
74
+ if (t && n !== void 0) return "A payment has either a Stripe price or an amount, not both.";
75
+ if (r !== "payment" && r !== "subscription") return "A payment is either one-time or a subscription.";
76
+ if (!Number.isInteger(i) || i < 1 || i > 999) return "Quantity must be a whole number between 1 and 999.";
77
+ if (n !== void 0) {
78
+ if (r === "subscription") return "A subscription needs a price created in Stripe — a typed amount cannot recur.";
79
+ if (typeof n != "number" || !Number.isFinite(n)) return "That amount is not a number.";
80
+ if (n < .5) return `The smallest amount that can be charged is ${_}.`;
81
+ if (n > g) return `That amount is above the ${g} limit for typed prices. Use a price created in Stripe.`;
82
+ if (!e.productName || !String(e.productName).trim()) return "A typed amount needs a name for what is being bought.";
83
+ }
84
+ return t && !/^price_[A-Za-z0-9]+$/.test(t) ? "That does not look like a Stripe price id." : null;
85
+ }
86
+ function y(e) {
87
+ return Math.round(e * 100);
88
+ }
89
+ function b(e, t, n) {
90
+ let r = String(e || "").replace(/\/+$/, ""), i = (e, t) => {
91
+ let n = String(e || t);
92
+ return n.startsWith("/") ? n : `/${n}`;
93
+ };
94
+ return {
95
+ success: `${r}${i(t, "/payment-complete")}?session_id={CHECKOUT_SESSION_ID}`,
96
+ cancel: `${r}${i(n, "/payment-cancelled")}`
97
+ };
98
+ }
99
+ function x(e) {
100
+ return e === "active" || e === "trialing" || e === "past_due";
101
+ }
102
+ //#endregion
103
+ //#region src/server/selftest.ts
104
+ var S = (e) => !!(process.env[e] || "").trim();
105
+ function C(e) {
106
+ let t = (process.env.SELFTEST_TOKEN || "").trim();
107
+ return t ? String(e || "") === t : !0;
108
+ }
109
+ async function w() {
110
+ let e = [{
111
+ name: "Server",
112
+ status: "pass",
113
+ detail: "The backend is running and reachable."
114
+ }], t = d();
115
+ if (!t) e.push({
116
+ name: "Firebase project",
117
+ status: "fail",
118
+ detail: "VITE_FIREBASE_PROJECT_ID is not set, so the server cannot tell who is signed in."
119
+ });
120
+ else {
121
+ e.push({
122
+ name: "Firebase project",
123
+ status: "pass",
124
+ detail: `Signing in against ${t}.`
125
+ });
126
+ try {
127
+ let t = await l();
128
+ e.push({
129
+ name: "Sign-in verification",
130
+ status: "pass",
131
+ detail: `Google's signing keys are reachable (${Object.keys(t).length} in rotation).`
132
+ });
133
+ } catch (t) {
134
+ e.push({
135
+ name: "Sign-in verification",
136
+ status: "fail",
137
+ detail: `Could not reach Google's signing keys: ${t.message}`
138
+ });
139
+ }
140
+ }
141
+ if (!p()) e.push({
142
+ name: "Stripe",
143
+ status: "warn",
144
+ detail: "STRIPE_SECRET_KEY is not set. Payments are switched off until it is."
145
+ });
146
+ else {
147
+ let t = m();
148
+ try {
149
+ let n = await h().accounts.retrieve();
150
+ e.push({
151
+ name: "Stripe",
152
+ status: "pass",
153
+ detail: `Connected in ${t} mode to ${n.business_profile?.name || n.id}.`
154
+ }), t === "live" && e.push({
155
+ name: "Live mode",
156
+ status: "warn",
157
+ detail: "This app is charging real cards. Every payment made here is real money."
158
+ });
159
+ } catch (t) {
160
+ e.push({
161
+ name: "Stripe",
162
+ status: "fail",
163
+ detail: `The key is set but Stripe refused it: ${t.message}`
164
+ });
165
+ }
166
+ e.push(S("STRIPE_WEBHOOK_SECRET") ? {
167
+ name: "Stripe webhook",
168
+ status: "pass",
169
+ detail: "Signed webhooks will be accepted."
170
+ } : {
171
+ name: "Stripe webhook",
172
+ status: "warn",
173
+ detail: "STRIPE_WEBHOOK_SECRET is not set. Payments still confirm when the payer returns; refunds and cancellations made in the Stripe dashboard will not reach the app until it is."
174
+ });
175
+ }
176
+ return e.push(S("RESEND_API_KEY") ? {
177
+ name: "Email",
178
+ status: "pass",
179
+ detail: "RESEND_API_KEY is set."
180
+ } : {
181
+ name: "Email",
182
+ status: "warn",
183
+ detail: "RESEND_API_KEY is not set. The app will not send its own email."
184
+ }), {
185
+ ok: !e.some((e) => e.status === "fail"),
186
+ service: "your-ai-workflow-firebase-os",
187
+ checks: e
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/server/index.ts
192
+ var T = (e, t, n, r = {}) => e.status(t).json({
193
+ success: !1,
194
+ error: n,
195
+ ...r
196
+ });
197
+ function E(e, t) {
198
+ let n = t instanceof Error ? t.message : "Something went wrong on the server.", r = n.includes("STRIPE_SECRET_KEY") || n.includes("project id");
199
+ return T(e, r ? 503 : 500, n, r ? { requiresSetup: !0 } : {});
200
+ }
201
+ async function D(e, t, n) {
202
+ let r = String(e.headers.authorization || ""), i = r.startsWith("Bearer ") ? r.slice(7) : "";
203
+ if (!i) return T(t, 401, "Please sign in first.");
204
+ try {
205
+ e.user = await u(i, d()), n();
206
+ } catch (e) {
207
+ return T(t, 401, e.message);
208
+ }
209
+ }
210
+ async function O(e, t) {
211
+ return !t.email || !t.emailVerified ? null : (await e.customers.list({
212
+ email: t.email,
213
+ limit: 1
214
+ })).data[0] || null;
215
+ }
216
+ async function k(e, t) {
217
+ let n = await O(e, t);
218
+ return n ? (n.metadata?.uid !== t.uid && await e.customers.update(n.id, { metadata: {
219
+ ...n.metadata,
220
+ uid: t.uid
221
+ } }), n) : e.customers.create({
222
+ email: t.email || void 0,
223
+ metadata: { uid: t.uid }
224
+ });
225
+ }
226
+ function A(e) {
227
+ return e?.current_period_end ?? e?.items?.data?.[0]?.current_period_end ?? null;
228
+ }
229
+ function j(e) {
230
+ let t = e.items?.data?.[0], n = t?.price;
231
+ return {
232
+ id: e.id,
233
+ status: e.status,
234
+ active: x(e.status),
235
+ priceId: n?.id || null,
236
+ productName: typeof n?.product == "object" && n.product?.name || null,
237
+ amount: n?.unit_amount == null ? null : n.unit_amount / 100,
238
+ currency: n?.currency || null,
239
+ interval: n?.recurring?.interval || null,
240
+ quantity: t?.quantity ?? 1,
241
+ currentPeriodEnd: A(e),
242
+ cancelAtPeriodEnd: !!e.cancel_at_period_end
243
+ };
244
+ }
245
+ function M(e) {
246
+ return {
247
+ id: e.id,
248
+ amount: e.amount / 100,
249
+ currency: e.currency,
250
+ created: e.created,
251
+ description: e.description || null,
252
+ paid: e.paid && e.status === "succeeded",
253
+ refunded: e.refunded,
254
+ amountRefunded: e.amount_refunded / 100,
255
+ fullyRefunded: e.refunded && e.amount_refunded >= e.amount
256
+ };
257
+ }
258
+ var N = /* @__PURE__ */ new Set();
259
+ function P(e) {
260
+ return N.has(e) ? !0 : (N.add(e), N.size > 500 && N.delete(N.values().next().value), !1);
261
+ }
262
+ function F(t = {}) {
263
+ let n = e.Router();
264
+ return n.post("/payments/webhook", e.raw({ type: "application/json" }), async (e, n) => {
265
+ let r = (process.env.STRIPE_WEBHOOK_SECRET || "").trim(), i = e.headers["stripe-signature"];
266
+ if (!r) return T(n, 503, "STRIPE_WEBHOOK_SECRET is not set.");
267
+ if (!i) return T(n, 400, "Missing stripe-signature header.");
268
+ let a;
269
+ try {
270
+ a = h().webhooks.constructEvent(e.body, i, r);
271
+ } catch (e) {
272
+ return T(n, 400, `Webhook signature check failed: ${e.message}`);
273
+ }
274
+ if (P(a.id)) return n.json({
275
+ received: !0,
276
+ duplicate: !0
277
+ });
278
+ try {
279
+ await t.onStripeEvent?.(a);
280
+ } catch (e) {
281
+ return N.delete(a.id), T(n, 503, `Handler failed: ${e.message}`);
282
+ }
283
+ return n.json({ received: !0 });
284
+ }), n.use(e.json()), n.get("/_selftest", async (e, t) => {
285
+ if (!C(e.query.token)) return T(t, 403, "This check needs its token.");
286
+ try {
287
+ let e = await w();
288
+ return t.status(e.ok ? 200 : 500).json(e);
289
+ } catch (e) {
290
+ return E(t, e);
291
+ }
292
+ }), n.get("/payments/status", (e, t) => {
293
+ let n = m();
294
+ t.json({
295
+ configured: !!p(),
296
+ mode: n,
297
+ webhookConfigured: !!(process.env.STRIPE_WEBHOOK_SECRET || "").trim(),
298
+ message: n ? `Stripe is connected in ${n} mode.` : "Add STRIPE_SECRET_KEY to start taking payments."
299
+ });
300
+ }), n.post("/payments/checkout", async (e, t) => {
301
+ let n = e.body || {}, r = v(n);
302
+ if (r) return T(t, 400, r);
303
+ let i = n.origin || e.headers.origin;
304
+ if (!i) return T(t, 400, "The page did not say where to send the payer back to.");
305
+ try {
306
+ let r = h(), a = null, o = String(e.headers.authorization || "");
307
+ if (o.startsWith("Bearer ")) try {
308
+ a = await u(o.slice(7), d());
309
+ } catch {
310
+ a = null;
311
+ }
312
+ let s = a ? await k(r, a) : null, c = b(String(i), n.successPath, n.cancelPath), l = n.priceId ? {
313
+ price: n.priceId,
314
+ quantity: n.quantity || 1
315
+ } : {
316
+ quantity: n.quantity || 1,
317
+ price_data: {
318
+ currency: n.currency || "usd",
319
+ product_data: { name: String(n.productName) },
320
+ unit_amount: y(Number(n.amount))
321
+ }
322
+ }, f = await r.checkout.sessions.create({
323
+ mode: n.mode || "payment",
324
+ line_items: [l],
325
+ success_url: c.success,
326
+ cancel_url: c.cancel,
327
+ customer: s?.id,
328
+ allow_promotion_codes: !0,
329
+ metadata: {
330
+ ...n.metadata || {},
331
+ ...a ? { uid: a.uid } : {}
332
+ }
333
+ });
334
+ return t.json({
335
+ success: !0,
336
+ sessionId: f.id,
337
+ url: f.url
338
+ });
339
+ } catch (e) {
340
+ return E(t, e);
341
+ }
342
+ }), n.get("/payments/session/:id", async (e, t) => {
343
+ try {
344
+ let n = await h().checkout.sessions.retrieve(String(e.params.id));
345
+ return t.json({
346
+ success: !0,
347
+ paid: n.payment_status === "paid",
348
+ paymentStatus: n.payment_status,
349
+ mode: n.mode,
350
+ amountTotal: n.amount_total == null ? null : n.amount_total / 100,
351
+ currency: n.currency,
352
+ customerEmail: n.customer_details?.email || null,
353
+ customerName: n.customer_details?.name || null,
354
+ metadata: n.metadata || {}
355
+ });
356
+ } catch (e) {
357
+ return (e instanceof Error ? e.message : "Unknown error").includes("STRIPE_SECRET_KEY") ? E(t, e) : T(t, 404, "That payment could not be found.");
358
+ }
359
+ }), n.get("/payments/me", D, async (e, t) => {
360
+ try {
361
+ let n = h(), r = await O(n, e.user);
362
+ if (!r) return t.json({
363
+ success: !0,
364
+ customerId: null,
365
+ active: !1,
366
+ subscriptions: [],
367
+ payments: []
368
+ });
369
+ let [i, a] = await Promise.all([n.subscriptions.list({
370
+ customer: r.id,
371
+ status: "all",
372
+ limit: 10,
373
+ expand: ["data.items.data.price.product"]
374
+ }), n.charges.list({
375
+ customer: r.id,
376
+ limit: 20
377
+ })]), o = i.data.map(j), s = a.data.map(M);
378
+ return t.json({
379
+ success: !0,
380
+ customerId: r.id,
381
+ email: r.email,
382
+ active: o.some((e) => e.active) || s.some((e) => e.paid && !e.fullyRefunded),
383
+ subscriptions: o,
384
+ payments: s
385
+ });
386
+ } catch (e) {
387
+ return E(t, e);
388
+ }
389
+ }), n.post("/payments/portal", D, async (e, t) => {
390
+ try {
391
+ let n = h(), r = await O(n, e.user);
392
+ if (!r) return T(t, 404, "There is nothing to manage on this account yet.");
393
+ let i = e.body?.origin || e.headers.origin;
394
+ if (!i) return T(t, 400, "The page did not say where to send them back to.");
395
+ let a = await n.billingPortal.sessions.create({
396
+ customer: r.id,
397
+ return_url: String(i).replace(/\/+$/, "")
398
+ });
399
+ return t.json({
400
+ success: !0,
401
+ url: a.url
402
+ });
403
+ } catch (e) {
404
+ return E(t, e);
405
+ }
406
+ }), n;
407
+ }
408
+ //#endregion
409
+ export { g as MAX_ADHOC_AMOUNT, _ as MIN_ADHOC_AMOUNT, F as apiRoutes, v as checkoutProblem, x as grantsAccess, d as projectId, b as returnUrls, w as runSelfTest, C as selfTestAllowed, m as stripeMode, u as verifyIdToken };
package/dist/vite.cjs.js CHANGED
@@ -173,4 +173,4 @@ if (typeof window !== 'undefined') {
173
173
  export const pagesRegistry = pages;
174
174
  export const formsRegistry = forms;
175
175
  export const microcomponentRegistry = microcomponents;
176
- `}var j=e=>A(T(e)),M=e=>e.split(`/`).pop()||e,N=e=>/\/configs\/[^/]+$/.test(e),P=e=>/\/microcomponents\/[^/]+\.tsx$/.test(e),F=e=>e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:\\'"`])\/\/.*$/gm,`$1`),I=(e,t)=>{let n=RegExp(`(?:^|[\\s,{])${t}\\s*:\\s*([^,\\n}]+)`,`g`),r=[],i;for(;i=n.exec(e);)r.push(i[1].trim());return r},L=e=>{let t=e.match(/^(['"`])([^'"`]*)\1$/);return t?t[2]:null},ne=e=>/export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e)||/export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e),R=e=>/export\s+default\b/.test(e),z=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``),B=[`board`,`table`,`calendar`,`drive`],re={board:{call:`actions.openInlineEditor(categoryId)`,token:`openInlineEditor`},table:{call:`actions.addRow()`,token:`addRow`},calendar:{call:`actions.openAddModal()`,token:`openAddModal`},drive:{call:`the openUploadModal() prop`,token:`openUploadModal`}},ie=/(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i,ae=e=>{let t=I(e,`route`);return{first:t[0],count:t.length}};function V(e){let t=[],n=[],r=new Map,i=new Set;for(let{file:t}of e)if(P(t))for(let e of y(M(t)))i.add(e);let a=new Map;for(let{file:t,source:n}of e){if(!N(t))continue;let e=M(t),r=x(e);if(r!==`tab`&&r!==`page`)continue;let i=F(n),o=I(i,`template`)[0],s=o===void 0?null:L(o);if(!s||!B.includes(s))continue;let c=I(i,r===`tab`?`tabName`:`pageName`)[0],l=c===void 0?null:L(c),u=[r===`tab`?g(e):_(e),...l?[z(l),z(l).replace(/_/g,``)]:[]];for(let e of u)a.set(e,s)}for(let{file:o,source:s}of e){let e=M(o),c=F(s);if(N(o)){let n=x(e);if(n===`shipped`)continue;if(n===`other`){t.push(`${o}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone) or somethingForm.config.ts (a form).`);continue}if(!ne(c)&&!R(c)&&t.push(`${o}: export one const named ${b(e)}Config.`),n===`tab`||n===`page`){let{first:a,count:s}=ae(c),l=a===void 0?null:L(a);if(s===0||l===null||!l.startsWith(`/`))t.push(`${o}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);else{let e=r.get(l);e?t.push(`${o}: route '${l}' is already used by ${e}. One page, one file, one address.`):r.set(l,o)}let u=I(c,`template`)[0],d=u===void 0?null:L(u);if(!d||![`board`,`table`,`calendar`,`drive`,`inline_form`,`popup_form`].includes(d)){let r=n===`tab`?g(e):_(e),a=I(c,n===`tab`?`tabName`:`pageName`)[0],s=a===void 0?null:L(a),l=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``);if(![r,...s?[l(s),l(s).replace(/_/g,``)]:[]].some(e=>i.has(e))){let n=b(e).replace(/(Tab|Page)$/,``),r=(s||n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``);t.push(`${o}: no content file matches this page, so it renders empty — create src/your-ai-workflow-firebase-os/microcomponents/${r}Content.tsx with export default.`)}}}if(n===`tab`){let e=I(c,`pageType`)[0],n=e===void 0?null:L(e);n!==`user`&&n!==`internal`&&n!==`admin`&&t.push(`${o}: pageType must be 'user', 'internal' or 'admin'. A dashboard page is never public.`)}if(n===`form`){let n=v(e),r=I(c,`systemName`)[0];r!==void 0&&L(r)!==n&&t.push(`${o}: systemName must be '${n}'.`)}continue}if(P(o)){R(c)||t.push(`${o}: add export default.`);let r=y(e).map(e=>a.get(e)).find(Boolean),i=/<form\b/.test(c)||/\bonSubmit\b/.test(c);if(i&&r){let e=re[r];c.includes(e.token)||n.push(`${o}: this page is a ${r} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`)}else i&&!/ContactPopup/.test(c)&&n.push(`${o}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);let s=c.match(/['"`](?:mem|user|admin|pub)_notifications['"`]/);s&&t.push(`${o}: ${s[0]} is the bell's own collection and pages never touch it — delete that code and send the notification with notify({ toUid | toRole, title, body, link }) imported from 'your-ai-workflow-firebase-os'.`),/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let l=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);l&&n.push(`${o}: ${l[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);let u=c.match(ie);u&&n.push(`${o}: "${u[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);let d=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);d&&n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);continue}if(o===p){/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS — while the home page is open it restyles every page of the app, the titles and the menu included — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let e=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);e&&n.push(`${o}: ${e[0]} makes the page exactly one screen tall and strands everything below it on a phone — let the page be as long as it needs and drop the height and any overflow-hidden around it.`);let r=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);r&&n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`)}}return{errors:t,warnings:n}}var oe=[`--bg-color`,`--bg-secondary-color`,`--fg-color`,`--accent-color`,`--panel-bg`,`--panel-border`,`--button-bg`,`--button-fg`,`--button-hover-bg`,`--btn-sec-bg`,`--btn-sec-border`,`--btn-sec-text`,`--text-gradient`,`--glow-color`,`--notification-icon-color`,`--selection-bg`,`--selection-text`],H=(e,t)=>{let n=[],r=t.replace(/\/\*[\s\S]*?\*\//g,``);for(let[,t,i]of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)){let r=t.trim(),a=oe.filter(e=>RegExp(`${e}\\s*:[^;}]*!important`).test(i));if(a.length){n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);continue}let o=/(?:^|[\s;])(?:font-size|line-height|font)\s*:/.test(i);if(/\.page-title\b/.test(r)){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` styles the app's page title, which is the library's chrome and not this file's to restyle — delete the rule; a page heading you want to size is one you write yourself inside a content file.`);continue}let s=i.match(/(?:^|[\s;])font-family\s*:\s*([^;}]+)/);if(s&&!/^\s*['"]?Inter['"]?\s*(?:,|$)/i.test(s[1])){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` sets a \`font-family\`, and the app has one typeface — Inter, everywhere, with no setting for it. Delete the \`font-family\` line; the rest of the rule can stay.`);continue}let c=r.split(`,`).map(e=>e.trim()).filter(e=>/^h[1-3]$/.test(e));c.length&&o&&n.push(`${e}: this rule sets a size on \`${c.join(`, `)}\`, which changes every page title in the app — delete the size (font-weight here is fine) and size the headings you write inside your own content file instead.`)}return n};function U(e,t){return V(e.map(e=>({file:e.file,source:t(e.abs)})))}var W=i,G=`\0`+W,K=[{rel:`src/App.tsx`,source:a},{rel:`src/main.tsx`,source:l}],q=e=>`${e} is owned by Your AI Firebase; your edit was discarded.`,J=e=>e.replace(/[?#].*$/,``),Y=e=>n.normalize(e).split(n.sep).join(`/`),X={info:e=>console.log(e),warn:e=>console.warn(e),error:e=>console.error(e)};function Z(e,r=X){let i=[];for(let{rel:a,source:o}of K){let s=n.join(e,a),c=null;try{c=t.readFileSync(s,`utf8`)}catch{c=null}c!==o&&(t.mkdirSync(n.dirname(s),{recursive:!0}),t.writeFileSync(s,o),i.push(a),c!==null&&r.warn(q(a)))}return i}function Q(e){let r=n.join(e,f,`registry.ts`);return t.existsSync(r)?!1:(t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,o),!0)}var $=(e,t)=>{let r=Y(J(t));for(let{rel:t,source:i}of K)if(r===Y(n.join(e,t)))return i;return null};function se(){let e=process.cwd(),r=!1,i=`build`,a=X,o=()=>{let r=U(T(e),e=>t.readFileSync(e,`utf8`)),i=n.join(e,`src`,`your-ai-workflow-firebase-os`,`theme.css`),a=t.existsSync(i)?H(`src/your-ai-workflow-firebase-os/theme.css`,t.readFileSync(i,`utf8`)):[];return{...r,errors:[...r.errors,...O(e),...a]}};return{name:`your-ai-firebase`,enforce:`pre`,config:()=>({resolve:{dedupe:[`firebase`,`react`,`react-dom`,`react-router-dom`]}}),configResolved(t){e=t.root,i=t.command,a=t.logger,r=C(e),r&&(Z(e,a),Q(e))},resolveId(e){return e===W?G:null},buildStart(){if(!r)return;let{errors:e,warnings:t}=o();for(let e of t)this.warn(e);if(e.length!==0)if(i===`build`)this.error(e[0]);else for(let t of e)a.error(t)},load(t){if(!r)return null;if(t===G){let t=T(e),{errors:n}=o();return n.length&&this.error(n[0]),A(t)}return $(e,t)},transform(e,t){if(!r||!Y(J(t)).endsWith(`src/your-ai-workflow-firebase-os/theme.css`))return null;let n=c(e);return n===e?null:{code:n,map:null}},configureServer(t){if(!r)return;let i=()=>{let e=t.moduleGraph.getModuleById(G);e&&t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`,path:`*`})},a=r=>{let a=Y(r);if($(e,a)!==null){Z(e,t.config.logger);return}if(!w(e,a)&&!te(a))return;n.relative(e,a).split(n.sep).join(`/`)!==p&&i();let{errors:s,warnings:c}=o();for(let e of c)t.config.logger.warn(e);for(let e of s)t.config.logger.error(e)};t.watcher.on(`add`,a),t.watcher.on(`unlink`,a),t.watcher.on(`change`,a)}}}exports.CANONICAL_APP_TSX=a,exports.CANONICAL_MAIN_TSX=l,exports.CONFIGS_DIR=d,exports.CONSUMER_DIR=u,exports.CONSUMER_REGISTRY_TS=o,exports.MICROCOMPONENTS_DIR=f,exports.OWNED_BANNER=r,exports.RESOLVED_VIRTUAL_ID=G,exports.SHIPPED_CONFIG_FILES=m,exports.VIRTUAL_ID=W,exports.VIRTUAL_REGISTRIES_ID=i,exports.classifyConfigFile=x,exports.discover=T,exports.ensureConsumerRegistry=Q,exports.fileIdFromConfigFile=b,exports.formIdFromFile=v,exports.isConsumerRoot=C,exports.isDiscoveredPath=w,exports.microcomponentKeysFromFile=y,exports.ownedMessage=q,exports.pageIdFromFile=_,exports.registrable=E,exports.restoreOwnedFiles=Z,exports.stripComments=F,exports.tabIdFromFile=g,exports.validate=V,exports.validateDiscovered=U,exports.virtualModuleSource=j,exports.virtualModuleSourceFor=A,exports.yourAiFirebase=se;
176
+ `}var j=e=>A(T(e)),M=e=>e.split(`/`).pop()||e,N=e=>/\/configs\/[^/]+$/.test(e),P=e=>/\/microcomponents\/[^/]+\.tsx$/.test(e),F=e=>e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:\\'"`])\/\/.*$/gm,`$1`),I=(e,t)=>{let n=RegExp(`(?:^|[\\s,{])${t}\\s*:\\s*([^,\\n}]+)`,`g`),r=[],i;for(;i=n.exec(e);)r.push(i[1].trim());return r},L=e=>{let t=e.match(/^(['"`])([^'"`]*)\1$/);return t?t[2]:null},ne=e=>/export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e)||/export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e),R=e=>/export\s+default\b/.test(e),z=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``),B=[`board`,`table`,`calendar`,`drive`],re={board:{call:`actions.openInlineEditor(categoryId)`,token:`openInlineEditor`},table:{call:`actions.addRow()`,token:`addRow`},calendar:{call:`actions.openAddModal()`,token:`openAddModal`},drive:{call:`the openUploadModal() prop`,token:`openUploadModal`}},ie=/(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i,ae=e=>{let t=I(e,`route`);return{first:t[0],count:t.length}};function V(e){let t=[],n=[],r=new Map,i=new Set;for(let{file:t}of e)if(P(t))for(let e of y(M(t)))i.add(e);let a=new Map;for(let{file:t,source:n}of e){if(!N(t))continue;let e=M(t),r=x(e);if(r!==`tab`&&r!==`page`)continue;let i=F(n),o=I(i,`template`)[0],s=o===void 0?null:L(o);if(!s||!B.includes(s))continue;let c=I(i,r===`tab`?`tabName`:`pageName`)[0],l=c===void 0?null:L(c),u=[r===`tab`?g(e):_(e),...l?[z(l),z(l).replace(/_/g,``)]:[]];for(let e of u)a.set(e,s)}for(let{file:o,source:s}of e){let e=M(o),c=F(s);if(N(o)){let n=x(e);if(n===`shipped`)continue;if(n===`other`){t.push(`${o}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone) or somethingForm.config.ts (a form).`);continue}if(!ne(c)&&!R(c)&&t.push(`${o}: export one const named ${b(e)}Config.`),n===`tab`||n===`page`){let{first:a,count:s}=ae(c),l=a===void 0?null:L(a);if(s===0||l===null||!l.startsWith(`/`))t.push(`${o}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);else{let e=r.get(l);e?t.push(`${o}: route '${l}' is already used by ${e}. One page, one file, one address.`):r.set(l,o)}let u=I(c,`template`)[0],d=u===void 0?null:L(u);if(!d||![`board`,`table`,`calendar`,`drive`,`inline_form`,`popup_form`].includes(d)){let r=n===`tab`?g(e):_(e),a=I(c,n===`tab`?`tabName`:`pageName`)[0],s=a===void 0?null:L(a),l=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``);if(![r,...s?[l(s),l(s).replace(/_/g,``)]:[]].some(e=>i.has(e))){let n=b(e).replace(/(Tab|Page)$/,``),r=(s||n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``);t.push(`${o}: no content file matches this page, so it renders empty — create src/your-ai-workflow-firebase-os/microcomponents/${r}Content.tsx with export default.`)}}}if(n===`tab`){let e=I(c,`pageType`)[0],n=e===void 0?null:L(e);n!==`user`&&n!==`internal`&&n!==`admin`&&t.push(`${o}: pageType must be 'user', 'internal' or 'admin'. A dashboard page is never public.`)}if(n===`form`){let n=v(e),r=I(c,`systemName`)[0];r!==void 0&&L(r)!==n&&t.push(`${o}: systemName must be '${n}'.`)}continue}if(P(o)){R(c)||t.push(`${o}: add export default.`);let r=y(e).map(e=>a.get(e)).find(Boolean),i=/<form\b/.test(c)||/\bonSubmit\b/.test(c);if(i&&r){let e=re[r];c.includes(e.token)||n.push(`${o}: this page is a ${r} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`)}else i&&!/ContactPopup/.test(c)&&n.push(`${o}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);let s=c.match(/['"`](?:mem|user|admin|pub)_notifications['"`]/);s&&t.push(`${o}: ${s[0]} is the bell's own collection and pages never touch it — delete that code and send the notification with notify({ toUid | toRole, title, body, link }) imported from 'your-ai-workflow-firebase-os'.`),/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let l=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);l&&n.push(`${o}: ${l[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);let u=c.match(ie);u&&n.push(`${o}: "${u[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);let d=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);d&&n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`),/<select[\s/>]/.test(c)&&n.push(`${o}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`);continue}if(o===p){/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS — while the home page is open it restyles every page of the app, the titles and the menu included — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let e=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);e&&n.push(`${o}: ${e[0]} makes the page exactly one screen tall and strands everything below it on a phone — let the page be as long as it needs and drop the height and any overflow-hidden around it.`);let r=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);r&&n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`),/<select[\s/>]/.test(c)&&n.push(`${o}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`)}}return{errors:t,warnings:n}}var oe=[`--bg-color`,`--bg-secondary-color`,`--fg-color`,`--accent-color`,`--panel-bg`,`--panel-border`,`--button-bg`,`--button-fg`,`--button-hover-bg`,`--btn-sec-bg`,`--btn-sec-border`,`--btn-sec-text`,`--text-gradient`,`--glow-color`,`--notification-icon-color`,`--selection-bg`,`--selection-text`],H=(e,t)=>{let n=[],r=t.replace(/\/\*[\s\S]*?\*\//g,``);for(let[,t,i]of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)){let r=t.trim(),a=oe.filter(e=>RegExp(`${e}\\s*:[^;}]*!important`).test(i));if(a.length){n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);continue}let o=/(?:^|[\s;])(?:font-size|line-height|font)\s*:/.test(i);if(/\.page-title\b/.test(r)){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` styles the app's page title, which is the library's chrome and not this file's to restyle — delete the rule; a page heading you want to size is one you write yourself inside a content file.`);continue}let s=i.match(/(?:^|[\s;])font-family\s*:\s*([^;}]+)/);if(s&&!/^\s*['"]?Inter['"]?\s*(?:,|$)/i.test(s[1])){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` sets a \`font-family\`, and the app has one typeface — Inter, everywhere, with no setting for it. Delete the \`font-family\` line; the rest of the rule can stay.`);continue}let c=r.split(`,`).map(e=>e.trim()).filter(e=>/^h[1-3]$/.test(e));c.length&&o&&n.push(`${e}: this rule sets a size on \`${c.join(`, `)}\`, which changes every page title in the app — delete the size (font-weight here is fine) and size the headings you write inside your own content file instead.`)}return n};function U(e,t){return V(e.map(e=>({file:e.file,source:t(e.abs)})))}var W=i,G=`\0`+W,K=[{rel:`src/App.tsx`,source:a},{rel:`src/main.tsx`,source:l}],q=e=>`${e} is owned by Your AI Firebase; your edit was discarded.`,J=e=>e.replace(/[?#].*$/,``),Y=e=>n.normalize(e).split(n.sep).join(`/`),X={info:e=>console.log(e),warn:e=>console.warn(e),error:e=>console.error(e)};function Z(e,r=X){let i=[];for(let{rel:a,source:o}of K){let s=n.join(e,a),c=null;try{c=t.readFileSync(s,`utf8`)}catch{c=null}c!==o&&(t.mkdirSync(n.dirname(s),{recursive:!0}),t.writeFileSync(s,o),i.push(a),c!==null&&r.warn(q(a)))}return i}function Q(e){let r=n.join(e,f,`registry.ts`);return t.existsSync(r)?!1:(t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,o),!0)}var $=(e,t)=>{let r=Y(J(t));for(let{rel:t,source:i}of K)if(r===Y(n.join(e,t)))return i;return null};function se(){let e=process.cwd(),r=!1,i=`build`,a=X,o=()=>{let r=U(T(e),e=>t.readFileSync(e,`utf8`)),i=n.join(e,`src`,`your-ai-workflow-firebase-os`,`theme.css`),a=t.existsSync(i)?H(`src/your-ai-workflow-firebase-os/theme.css`,t.readFileSync(i,`utf8`)):[];return{...r,errors:[...r.errors,...O(e),...a]}};return{name:`your-ai-firebase`,enforce:`pre`,config:()=>({resolve:{dedupe:[`firebase`,`react`,`react-dom`,`react-router-dom`]}}),configResolved(t){e=t.root,i=t.command,a=t.logger,r=C(e),r&&(Z(e,a),Q(e))},resolveId(e){return e===W?G:null},buildStart(){if(!r)return;let{errors:e,warnings:t}=o();for(let e of t)this.warn(e);if(e.length!==0)if(i===`build`)this.error(e[0]);else for(let t of e)a.error(t)},load(t){if(!r)return null;if(t===G){let t=T(e),{errors:n}=o();return n.length&&this.error(n[0]),A(t)}return $(e,t)},transform(e,t){if(!r||!Y(J(t)).endsWith(`src/your-ai-workflow-firebase-os/theme.css`))return null;let n=c(e);return n===e?null:{code:n,map:null}},configureServer(t){if(!r)return;let i=()=>{let e=t.moduleGraph.getModuleById(G);e&&t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`,path:`*`})},a=r=>{let a=Y(r);if($(e,a)!==null){Z(e,t.config.logger);return}if(!w(e,a)&&!te(a))return;n.relative(e,a).split(n.sep).join(`/`)!==p&&i();let{errors:s,warnings:c}=o();for(let e of c)t.config.logger.warn(e);for(let e of s)t.config.logger.error(e)};t.watcher.on(`add`,a),t.watcher.on(`unlink`,a),t.watcher.on(`change`,a)}}}exports.CANONICAL_APP_TSX=a,exports.CANONICAL_MAIN_TSX=l,exports.CONFIGS_DIR=d,exports.CONSUMER_DIR=u,exports.CONSUMER_REGISTRY_TS=o,exports.MICROCOMPONENTS_DIR=f,exports.OWNED_BANNER=r,exports.RESOLVED_VIRTUAL_ID=G,exports.SHIPPED_CONFIG_FILES=m,exports.VIRTUAL_ID=W,exports.VIRTUAL_REGISTRIES_ID=i,exports.classifyConfigFile=x,exports.discover=T,exports.ensureConsumerRegistry=Q,exports.fileIdFromConfigFile=b,exports.formIdFromFile=v,exports.isConsumerRoot=C,exports.isDiscoveredPath=w,exports.microcomponentKeysFromFile=y,exports.ownedMessage=q,exports.pageIdFromFile=_,exports.registrable=E,exports.restoreOwnedFiles=Z,exports.stripComments=F,exports.tabIdFromFile=g,exports.validate=V,exports.validateDiscovered=U,exports.virtualModuleSource=j,exports.virtualModuleSourceFor=A,exports.yourAiFirebase=se;
package/dist/vite.es.js CHANGED
@@ -387,7 +387,7 @@ function H(e) {
387
387
  let u = c.match(V);
388
388
  u && n.push(`${o}: "${u[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);
389
389
  let d = c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);
390
- d && n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);
390
+ d && n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`), /<select[\s/>]/.test(c) && n.push(`${o}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`);
391
391
  continue;
392
392
  }
393
393
  if (o === f) {
@@ -395,7 +395,7 @@ function H(e) {
395
395
  let e = c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);
396
396
  e && n.push(`${o}: ${e[0]} makes the page exactly one screen tall and strands everything below it on a phone — let the page be as long as it needs and drop the height and any overflow-hidden around it.`);
397
397
  let r = c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);
398
- r && n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);
398
+ r && n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`), /<select[\s/>]/.test(c) && n.push(`${o}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`);
399
399
  }
400
400
  }
401
401
  return {