@pya-platform/shared 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @pya/shared
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a9ca6bf: Initial release of the Pya platform packages. Extracted from `pyaeats-app`, consumed by `pyaeats-app` (food delivery) and `pyaserv` (services classifieds).
8
+
9
+ Each package exposes a Hono router factory (auth/cms/reviews/comments) or a typed helper (email/audit/cf) parameterised over Cloudflare D1 + KV bindings. UI primitives ship as Lit web components on top of `@pya/tokens` (CSS custom properties). See `ROADMAP.md` and `docs/phase-6-rollout.md` for the consumer cutover plan.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@pya-platform/shared",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org",
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/undeadliner/pya-platform.git"
12
+ },
13
+ "type": "module",
14
+ "description": "Common Valibot schemas, id helpers, money/time primitives — shared by every Pya project.",
15
+ "exports": {
16
+ ".": "./src/index.ts",
17
+ "./schemas/*": "./src/schemas/*.ts"
18
+ },
19
+ "scripts": {
20
+ "type-check": "tsc --noEmit",
21
+ "test": "echo '@pya/shared has no tests yet'"
22
+ },
23
+ "dependencies": {
24
+ "effect": "^3.10.0",
25
+ "valibot": "^1.0.0"
26
+ }
27
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { Data } from 'effect'
2
+
3
+ /** Domain-tagged errors. Mapped to HTTP by `mapErrorToResponse`. */
4
+ export class UnauthorizedError extends Data.TaggedError('Unauthorized')<{
5
+ readonly reason: string
6
+ }> {}
7
+
8
+ export class ForbiddenError extends Data.TaggedError('Forbidden')<{
9
+ readonly required: string
10
+ }> {}
11
+
12
+ export class NotFoundError extends Data.TaggedError('NotFound')<{
13
+ readonly resource: string
14
+ readonly id?: string
15
+ }> {}
16
+
17
+ export class ValidationError extends Data.TaggedError('Validation')<{
18
+ readonly issues: readonly { readonly path: string; readonly message: string }[]
19
+ }> {}
20
+
21
+ export class ConflictError extends Data.TaggedError('Conflict')<{
22
+ readonly reason: string
23
+ }> {}
24
+
25
+ export class RateLimitedError extends Data.TaggedError('RateLimited')<{
26
+ readonly retryAfterSec: number
27
+ }> {}
28
+
29
+ export class UpstreamError extends Data.TaggedError('Upstream')<{
30
+ readonly provider: string
31
+ readonly status: number
32
+ }> {}
33
+
34
+ export class InvalidTokenError extends Data.TaggedError('InvalidToken')<{
35
+ readonly reason: string
36
+ }> {}
37
+
38
+ export class IdentityConflictError extends Data.TaggedError('IdentityConflict')<{
39
+ readonly provider: string
40
+ }> {}
41
+
42
+ export class ProviderNotEnabledError extends Data.TaggedError('ProviderNotEnabled')<{
43
+ readonly provider: string
44
+ }> {}
45
+
46
+ export type DomainError =
47
+ | UnauthorizedError
48
+ | ForbiddenError
49
+ | NotFoundError
50
+ | ValidationError
51
+ | ConflictError
52
+ | RateLimitedError
53
+ | UpstreamError
54
+ | InvalidTokenError
55
+ | IdentityConflictError
56
+ | ProviderNotEnabledError
57
+
58
+ export const mapErrorToStatus = (error: DomainError): number => {
59
+ switch (error._tag) {
60
+ case 'Unauthorized':
61
+ return 401
62
+ case 'Forbidden':
63
+ return 403
64
+ case 'NotFound':
65
+ return 404
66
+ case 'Validation':
67
+ return 422
68
+ case 'Conflict':
69
+ return 409
70
+ case 'RateLimited':
71
+ return 429
72
+ case 'Upstream':
73
+ return 502
74
+ case 'InvalidToken':
75
+ return 401
76
+ case 'IdentityConflict':
77
+ return 409
78
+ case 'ProviderNotEnabled':
79
+ return 501
80
+ }
81
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // @pya-platform/shared — common Valibot schemas, id helpers, domain
2
+ // errors, type primitives. Consumed by every Pya project (PyaEats, PyaServ,
3
+ // …) and by the rest of the platform packages.
4
+
5
+ export * from './schemas/id.ts'
6
+ export * from './schemas/user.ts'
7
+ export * from './schemas/session.ts'
8
+ export * from './schemas/passkey.ts'
9
+ export * from './schemas/oauth.ts'
10
+ export * from './errors.ts'
@@ -0,0 +1,20 @@
1
+ import * as v from 'valibot'
2
+
3
+ export const UuidSchema = v.pipe(
4
+ v.string(),
5
+ v.regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'Invalid UUID'),
6
+ )
7
+ export type Uuid = v.InferOutput<typeof UuidSchema>
8
+
9
+ /** UUID v7 — time-ordered, sortable, monotonic enough for our load levels. */
10
+ export const uuidV7 = (): string => {
11
+ const ts = BigInt(Date.now())
12
+ const rand = crypto.getRandomValues(new Uint8Array(10))
13
+ const tsHex = ts.toString(16).padStart(12, '0')
14
+ const a = tsHex.slice(0, 8)
15
+ const b = tsHex.slice(8, 12)
16
+ const c = `7${((rand[0] ?? 0) & 0x0f).toString(16).padStart(1, '0')}${(rand[1] ?? 0).toString(16).padStart(2, '0')}`
17
+ const d = `${(((rand[2] ?? 0) & 0x3f) | 0x80).toString(16).padStart(2, '0')}${(rand[3] ?? 0).toString(16).padStart(2, '0')}`
18
+ const e = Array.from(rand.slice(4, 10), (x) => x.toString(16).padStart(2, '0')).join('')
19
+ return `${a}-${b}-${c}-${d}-${e}`
20
+ }
@@ -0,0 +1,12 @@
1
+ import * as v from 'valibot'
2
+ import { EmailSchema, IdentityProviderSchema } from './user.ts'
3
+
4
+ export const ProviderClaimsSchema = v.object({
5
+ provider: IdentityProviderSchema,
6
+ subject: v.pipe(v.string(), v.minLength(1), v.maxLength(255)),
7
+ email: EmailSchema,
8
+ emailVerified: v.boolean(),
9
+ displayName: v.optional(v.pipe(v.string(), v.maxLength(120))),
10
+ locale: v.optional(v.string()),
11
+ })
12
+ export type ProviderClaims = v.InferOutput<typeof ProviderClaimsSchema>
@@ -0,0 +1,32 @@
1
+ import * as v from 'valibot'
2
+ import { EmailSchema } from './user.ts'
3
+
4
+ export const StartBodySchema = v.object({
5
+ email: EmailSchema,
6
+ force: v.optional(v.picklist(['otp'])),
7
+ redirect: v.optional(v.string()),
8
+ })
9
+ export type StartBody = v.InferOutput<typeof StartBodySchema>
10
+
11
+ export const OtpVerifyBodySchema = v.object({
12
+ email: EmailSchema,
13
+ code: v.pipe(v.string(), v.regex(/^\d{6}$/)),
14
+ })
15
+ export type OtpVerifyBody = v.InferOutput<typeof OtpVerifyBodySchema>
16
+
17
+ /**
18
+ * Passkey authentication assertion — shape comes from SimpleWebAuthn library;
19
+ * we keep it as `record(unknown)` at the boundary and let the lib validate.
20
+ */
21
+ export const PasskeyAuthVerifyBodySchema = v.object({
22
+ email: EmailSchema,
23
+ assertion: v.record(v.string(), v.unknown()),
24
+ })
25
+ export type PasskeyAuthVerifyBody = v.InferOutput<typeof PasskeyAuthVerifyBodySchema>
26
+
27
+ export const PasskeyRegisterVerifyBodySchema = v.object({
28
+ challengeId: v.pipe(v.string(), v.minLength(1)),
29
+ attestation: v.record(v.string(), v.unknown()),
30
+ label: v.optional(v.pipe(v.string(), v.maxLength(80))),
31
+ })
32
+ export type PasskeyRegisterVerifyBody = v.InferOutput<typeof PasskeyRegisterVerifyBodySchema>
@@ -0,0 +1,27 @@
1
+ import * as v from 'valibot'
2
+ import { UuidSchema } from './id.ts'
3
+ import { RoleSchema } from './user.ts'
4
+
5
+ /** Stored in KV under key `sess:<sid>`. Never sent to the client. */
6
+ export const SessionRecordSchema = v.object({
7
+ userId: UuidSchema,
8
+ roles: v.array(RoleSchema),
9
+ storeIds: v.array(UuidSchema),
10
+ iat: v.number(),
11
+ lastSeen: v.number(),
12
+ ipHash: v.string(),
13
+ uaHash: v.string(),
14
+ mfaAt: v.optional(v.number()),
15
+ })
16
+ export type SessionRecord = v.InferOutput<typeof SessionRecordSchema>
17
+
18
+ /** Stored in KV under key `oauth:state:<state>`, TTL 600s. */
19
+ export const OAuthStateSchema = v.object({
20
+ verifier: v.string(),
21
+ provider: v.picklist(['google', 'apple', 'facebook']),
22
+ redirectAfter: v.string(),
23
+ nonce: v.string(),
24
+ intent: v.optional(v.picklist(['login', 'link'])),
25
+ currentUserId: v.optional(UuidSchema),
26
+ })
27
+ export type OAuthState = v.InferOutput<typeof OAuthStateSchema>
@@ -0,0 +1,61 @@
1
+ import * as v from 'valibot'
2
+ import { UuidSchema } from './id.ts'
3
+
4
+ /** Application roles. Customer is implicit when no other role applies.
5
+ * `system` is a synthetic actor used by cron / background jobs for
6
+ * transitions (e.g. auto-cancel after acknowledge window). */
7
+ export const RoleSchema = v.picklist([
8
+ 'customer',
9
+ 'store_owner',
10
+ 'store_staff',
11
+ 'courier',
12
+ 'admin',
13
+ 'super_admin',
14
+ 'system',
15
+ ])
16
+ export type Role = v.InferOutput<typeof RoleSchema>
17
+
18
+ export const OAuthProviderSchema = v.picklist(['google', 'apple', 'facebook'])
19
+ export type OAuthProvider = v.InferOutput<typeof OAuthProviderSchema>
20
+
21
+ /** Includes email as an identity provider (magic link). */
22
+ export const IdentityProviderSchema = v.picklist(['google', 'apple', 'facebook', 'email'])
23
+ export type IdentityProvider = v.InferOutput<typeof IdentityProviderSchema>
24
+
25
+ export const EmailSchema = v.pipe(v.string(), v.email(), v.toLowerCase(), v.maxLength(254))
26
+ export type Email = v.InferOutput<typeof EmailSchema>
27
+
28
+ export const LocaleSchema = v.picklist(['es-PY', 'gn-PY'])
29
+ export type Locale = v.InferOutput<typeof LocaleSchema>
30
+
31
+ export const UserStatusSchema = v.picklist(['active', 'suspended', 'deleted'])
32
+ export type UserStatus = v.InferOutput<typeof UserStatusSchema>
33
+
34
+ export const UserSchema = v.object({
35
+ id: UuidSchema,
36
+ email: EmailSchema,
37
+ emailVerified: v.boolean(),
38
+ displayName: v.optional(v.string()),
39
+ locale: v.optional(LocaleSchema, 'es-PY'),
40
+ createdAt: v.number(),
41
+ status: UserStatusSchema,
42
+ })
43
+ export type User = v.InferOutput<typeof UserSchema>
44
+
45
+ export const UserIdentitySchema = v.object({
46
+ userId: UuidSchema,
47
+ provider: OAuthProviderSchema,
48
+ subject: v.string(),
49
+ emailAtLink: v.optional(EmailSchema),
50
+ linkedAt: v.number(),
51
+ })
52
+ export type UserIdentity = v.InferOutput<typeof UserIdentitySchema>
53
+
54
+ export const RoleAssignmentSchema = v.object({
55
+ userId: UuidSchema,
56
+ role: RoleSchema,
57
+ storeId: v.optional(UuidSchema),
58
+ grantedBy: v.optional(UuidSchema),
59
+ grantedAt: v.number(),
60
+ })
61
+ export type RoleAssignment = v.InferOutput<typeof RoleAssignmentSchema>
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "noEmit": true
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }