@pvh-afl/core 1.1.21 → 1.1.22

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.
Files changed (43) hide show
  1. package/dist/auth/auth.module.d.ts +58 -0
  2. package/dist/auth/auth.module.d.ts.map +1 -0
  3. package/dist/auth/auth.module.js +87 -0
  4. package/dist/auth/auth.module.js.map +1 -0
  5. package/dist/auth/auth.types.d.ts +122 -0
  6. package/dist/auth/auth.types.d.ts.map +1 -0
  7. package/dist/auth/auth.types.js +41 -0
  8. package/dist/auth/auth.types.js.map +1 -0
  9. package/dist/auth/current-user.decorator.d.ts +17 -0
  10. package/dist/auth/current-user.decorator.d.ts.map +1 -0
  11. package/dist/auth/current-user.decorator.js +22 -0
  12. package/dist/auth/current-user.decorator.js.map +1 -0
  13. package/dist/auth/index.d.ts +10 -0
  14. package/dist/auth/index.d.ts.map +1 -0
  15. package/dist/auth/index.js +19 -0
  16. package/dist/auth/index.js.map +1 -0
  17. package/dist/auth/jwt-auth.guard.d.ts +21 -0
  18. package/dist/auth/jwt-auth.guard.d.ts.map +1 -0
  19. package/dist/auth/jwt-auth.guard.js +84 -0
  20. package/dist/auth/jwt-auth.guard.js.map +1 -0
  21. package/dist/auth/jwt.service.d.ts +93 -0
  22. package/dist/auth/jwt.service.d.ts.map +1 -0
  23. package/dist/auth/jwt.service.js +185 -0
  24. package/dist/auth/jwt.service.js.map +1 -0
  25. package/dist/auth/optional-jwt-auth.guard.d.ts +34 -0
  26. package/dist/auth/optional-jwt-auth.guard.d.ts.map +1 -0
  27. package/dist/auth/optional-jwt-auth.guard.js +55 -0
  28. package/dist/auth/optional-jwt-auth.guard.js.map +1 -0
  29. package/dist/auth/ownership.d.ts +65 -0
  30. package/dist/auth/ownership.d.ts.map +1 -0
  31. package/dist/auth/ownership.js +117 -0
  32. package/dist/auth/ownership.js.map +1 -0
  33. package/dist/auth/utils.d.ts +53 -0
  34. package/dist/auth/utils.d.ts.map +1 -0
  35. package/dist/auth/utils.js +86 -0
  36. package/dist/auth/utils.js.map +1 -0
  37. package/dist/integrations/commerce/commerce.types.d.ts +4 -1
  38. package/dist/integrations/commerce/commerce.types.d.ts.map +1 -1
  39. package/dist/integrations/commerce/shopify.service.d.ts +2 -2
  40. package/dist/integrations/commerce/shopify.service.d.ts.map +1 -1
  41. package/dist/integrations/commerce/shopify.service.js +22 -4
  42. package/dist/integrations/commerce/shopify.service.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthTokenService = exports.TokenWrongTypeError = exports.TokenInvalidError = exports.TokenExpiredError = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const jwt_1 = require("@nestjs/jwt");
7
+ const auth_types_1 = require("./auth.types");
8
+ const utils_1 = require("./utils");
9
+ /** Thrown when a token is well-formed and correctly signed but has expired. */
10
+ class TokenExpiredError extends Error {
11
+ }
12
+ exports.TokenExpiredError = TokenExpiredError;
13
+ /** Thrown when a token fails any other check. */
14
+ class TokenInvalidError extends Error {
15
+ }
16
+ exports.TokenInvalidError = TokenInvalidError;
17
+ /** Thrown when a refresh token is used as an access token, or the reverse. */
18
+ class TokenWrongTypeError extends Error {
19
+ }
20
+ exports.TokenWrongTypeError = TokenWrongTypeError;
21
+ const DEFAULT_TTL_SECONDS = 1800; // 30 minutes
22
+ const DEFAULT_REFRESH_TTL_SECONDS = 604800; // 7 days
23
+ /**
24
+ * Signs and verifies the authentication tokens this service issues.
25
+ *
26
+ * Two kinds, distinguished by a `typ` claim that is checked on every verify:
27
+ *
28
+ * - **access** (30 min) — sent on every protected request.
29
+ * - **refresh** (7 days) — sent only to `/auth/refresh`, to mint a new access
30
+ * token without going back to Shopify.
31
+ *
32
+ * The refresh token carries the same identity claims as an access token, which is
33
+ * what lets renewal be a purely local operation: no Shopify call, so renewal
34
+ * costs nothing against Shopify's rate limit.
35
+ *
36
+ * Fixed to RS256, and the algorithm is pinned on verify rather than read from the
37
+ * token. That is deliberate: the JWT spec allows `alg: none`, and a verifier that
38
+ * trusts the token's own header can also be tricked into validating an RS256
39
+ * token as HS256 using the public key as the HMAC secret. Both attacks fail
40
+ * against an explicit allow-list of one algorithm.
41
+ */
42
+ let AuthTokenService = class AuthTokenService {
43
+ jwt;
44
+ options;
45
+ ttlSeconds;
46
+ refreshTtlSeconds;
47
+ constructor(jwt, options) {
48
+ this.jwt = jwt;
49
+ this.options = options;
50
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
51
+ this.refreshTtlSeconds =
52
+ options.refreshTtlSeconds ?? DEFAULT_REFRESH_TTL_SECONDS;
53
+ }
54
+ /**
55
+ * Issue an access and refresh token for a customer whose identity Shopify has
56
+ * already confirmed.
57
+ *
58
+ * @param customerId - Shopify customer GID, taken from Shopify's response.
59
+ * @param phone - The customer's phone number as Shopify holds it, if any.
60
+ * Stored as a hash so a `mobile` value in a later request can be checked
61
+ * against it without the number appearing in the token.
62
+ * @param maxExpiresAt - Optional upper bound on both expiries, normally an
63
+ * upstream credential's own expiry, so neither token outlives it.
64
+ */
65
+ async issuePair(customerId, phone, maxExpiresAt) {
66
+ const [access, refresh] = await Promise.all([
67
+ this.sign(auth_types_1.TokenType.ACCESS, customerId, phone, this.ttlSeconds, maxExpiresAt),
68
+ this.sign(auth_types_1.TokenType.REFRESH, customerId, phone, this.refreshTtlSeconds, maxExpiresAt),
69
+ ]);
70
+ return {
71
+ accessToken: access.token,
72
+ accessExpiresAt: access.expiresAt,
73
+ refreshToken: refresh.token,
74
+ refreshExpiresAt: refresh.expiresAt,
75
+ };
76
+ }
77
+ /**
78
+ * Issue a replacement access token from a verified refresh token.
79
+ *
80
+ * The refresh token itself is **not** replaced: its expiry is the absolute end
81
+ * of the session (see `refreshTtlSeconds`). The caller keeps using the same one
82
+ * until it expires, then logs in again.
83
+ */
84
+ async issueAccessToken(user, maxExpiresAt) {
85
+ return this.signFromHash(auth_types_1.TokenType.ACCESS, user.customerId, user.phoneHash, this.ttlSeconds, maxExpiresAt);
86
+ }
87
+ /**
88
+ * Verify an access token and return the caller it identifies.
89
+ *
90
+ * @throws {TokenExpiredError} past `exp` — the client should refresh and retry.
91
+ * @throws {TokenWrongTypeError} a refresh token was sent instead.
92
+ * @throws {TokenInvalidError} anything else.
93
+ */
94
+ async verifyAccess(token) {
95
+ return this.verify(token, auth_types_1.TokenType.ACCESS);
96
+ }
97
+ /**
98
+ * Verify a refresh token and return the caller it identifies.
99
+ *
100
+ * @throws {TokenExpiredError} the session has reached its absolute end; the
101
+ * customer has to log in again.
102
+ * @throws {TokenWrongTypeError} an access token was sent instead.
103
+ * @throws {TokenInvalidError} anything else.
104
+ */
105
+ async verifyRefresh(token) {
106
+ return this.verify(token, auth_types_1.TokenType.REFRESH);
107
+ }
108
+ async verify(token, expected) {
109
+ let payload;
110
+ try {
111
+ payload = await this.jwt.verifyAsync(token, {
112
+ // Pinned here, never taken from the token header.
113
+ algorithms: ['RS256'],
114
+ publicKey: this.options.publicKey,
115
+ issuer: this.options.issuer,
116
+ audience: this.options.audience,
117
+ });
118
+ }
119
+ catch (error) {
120
+ // jsonwebtoken signals expiry with this name; treat it separately so the
121
+ // client knows to refresh rather than to send the user back to login.
122
+ if (error instanceof Error && error.name === 'TokenExpiredError') {
123
+ throw new TokenExpiredError('Token has expired');
124
+ }
125
+ throw new TokenInvalidError(error instanceof Error ? error.message : 'Token verification failed');
126
+ }
127
+ // Checked before anything else about the payload: a valid signature on the
128
+ // wrong kind of token is still the wrong token.
129
+ if (payload.typ !== expected) {
130
+ throw new TokenWrongTypeError(`Expected a ${expected} token but received ${payload.typ ?? 'none'}`);
131
+ }
132
+ // `sub` carries the identity, so an absent or non-string value means the
133
+ // token cannot authenticate anyone even though its signature is valid.
134
+ if (typeof payload.sub !== 'string' || payload.sub.trim() === '') {
135
+ throw new TokenInvalidError('Token has no subject');
136
+ }
137
+ return {
138
+ customerId: payload.sub,
139
+ phoneHash: typeof payload.phone_hash === 'string' ? payload.phone_hash : undefined,
140
+ };
141
+ }
142
+ sign(typ, customerId, phone, ttlSeconds, maxExpiresAt) {
143
+ return this.signFromHash(typ, customerId, phone ? (0, utils_1.hashPhone)(phone) : undefined, ttlSeconds, maxExpiresAt);
144
+ }
145
+ async signFromHash(typ, customerId, phoneHash, ttlSeconds, maxExpiresAt) {
146
+ const nowSeconds = Math.floor(Date.now() / 1000);
147
+ const exp = this.resolveExpiry(nowSeconds, ttlSeconds, maxExpiresAt);
148
+ const payload = {
149
+ iss: this.options.issuer,
150
+ aud: this.options.audience,
151
+ typ,
152
+ sub: customerId,
153
+ ...(phoneHash ? { phone_hash: phoneHash } : {}),
154
+ iat: nowSeconds,
155
+ exp,
156
+ };
157
+ const token = await this.jwt.signAsync(payload, {
158
+ algorithm: 'RS256',
159
+ privateKey: this.options.privateKey,
160
+ // iss/aud/iat/exp are already in the payload; signing them again via
161
+ // options would make jsonwebtoken reject the duplicate keys.
162
+ });
163
+ return { token, expiresAt: new Date(exp * 1000) };
164
+ }
165
+ /**
166
+ * Work out the `exp` claim: the given lifetime, shortened if an upstream
167
+ * credential expires sooner.
168
+ */
169
+ resolveExpiry(nowSeconds, ttlSeconds, maxExpiresAt) {
170
+ const ownExpiry = nowSeconds + ttlSeconds;
171
+ if (!maxExpiresAt)
172
+ return ownExpiry;
173
+ const upstream = new Date(maxExpiresAt).getTime();
174
+ if (Number.isNaN(upstream))
175
+ return ownExpiry;
176
+ return Math.min(ownExpiry, Math.floor(upstream / 1000));
177
+ }
178
+ };
179
+ exports.AuthTokenService = AuthTokenService;
180
+ exports.AuthTokenService = AuthTokenService = tslib_1.__decorate([
181
+ (0, common_1.Injectable)(),
182
+ tslib_1.__param(1, (0, common_1.Inject)(auth_types_1.AUTH_OPTIONS)),
183
+ tslib_1.__metadata("design:paramtypes", [jwt_1.JwtService, Object])
184
+ ], AuthTokenService);
185
+ //# sourceMappingURL=jwt.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.service.js","sourceRoot":"","sources":["../../src/auth/jwt.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { AuthTokenService } from './jwt.service';
3
+ /**
4
+ * Attaches the caller when a token is supplied, and lets the request through when
5
+ * none is.
6
+ *
7
+ * For routes that legitimately serve both guests and signed-in customers — the
8
+ * wishlist, where a shopper builds a list before they have an account. Requiring a
9
+ * token there would break guest wishlists; ignoring tokens entirely would leave no
10
+ * way to tell the two apart.
11
+ *
12
+ * "Optional" means optional to *send*. A token that is present but unusable is still
13
+ * rejected, with the same error code {@link JwtAuthGuard} would give:
14
+ *
15
+ * - no token → treated as a guest, request proceeds with no `user`
16
+ * - valid token → caller attached to the request
17
+ * - expired or invalid token → 401, exactly as on a protected route
18
+ *
19
+ * Degrading a broken token to "guest" instead would hide two real problems: a client
20
+ * with an expired token would silently receive guest data rather than being told to
21
+ * refresh, and a genuine bug in token handling would look like ordinary guest
22
+ * traffic.
23
+ *
24
+ * Because a guest reaches the handler with no `user`, this must be paired with
25
+ * `assertOwnsClaimedCustomer` — otherwise a caller could simply name any customer
26
+ * and be served their data. The guard establishes *whether* there is a caller; that
27
+ * assertion enforces what an unproven claim is allowed to do.
28
+ */
29
+ export declare class OptionalJwtAuthGuard implements CanActivate {
30
+ private readonly strict;
31
+ constructor(tokens: AuthTokenService);
32
+ canActivate(context: ExecutionContext): Promise<boolean>;
33
+ }
34
+ //# sourceMappingURL=optional-jwt-auth.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optional-jwt-auth.guard.d.ts","sourceRoot":"","sources":["../../src/auth/optional-jwt-auth.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAc,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,oBAAqB,YAAW,WAAW;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAE1B,MAAM,EAAE,gBAAgB;IAM9B,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAS/D"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OptionalJwtAuthGuard = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const jwt_auth_guard_1 = require("./jwt-auth.guard");
7
+ const jwt_service_1 = require("./jwt.service");
8
+ const utils_1 = require("./utils");
9
+ /**
10
+ * Attaches the caller when a token is supplied, and lets the request through when
11
+ * none is.
12
+ *
13
+ * For routes that legitimately serve both guests and signed-in customers — the
14
+ * wishlist, where a shopper builds a list before they have an account. Requiring a
15
+ * token there would break guest wishlists; ignoring tokens entirely would leave no
16
+ * way to tell the two apart.
17
+ *
18
+ * "Optional" means optional to *send*. A token that is present but unusable is still
19
+ * rejected, with the same error code {@link JwtAuthGuard} would give:
20
+ *
21
+ * - no token → treated as a guest, request proceeds with no `user`
22
+ * - valid token → caller attached to the request
23
+ * - expired or invalid token → 401, exactly as on a protected route
24
+ *
25
+ * Degrading a broken token to "guest" instead would hide two real problems: a client
26
+ * with an expired token would silently receive guest data rather than being told to
27
+ * refresh, and a genuine bug in token handling would look like ordinary guest
28
+ * traffic.
29
+ *
30
+ * Because a guest reaches the handler with no `user`, this must be paired with
31
+ * `assertOwnsClaimedCustomer` — otherwise a caller could simply name any customer
32
+ * and be served their data. The guard establishes *whether* there is a caller; that
33
+ * assertion enforces what an unproven claim is allowed to do.
34
+ */
35
+ let OptionalJwtAuthGuard = class OptionalJwtAuthGuard {
36
+ strict;
37
+ constructor(tokens) {
38
+ // Delegate rather than duplicate, so the two guards cannot drift apart in how
39
+ // they verify a token or which error codes they return.
40
+ this.strict = new jwt_auth_guard_1.JwtAuthGuard(tokens);
41
+ }
42
+ async canActivate(context) {
43
+ const request = (0, utils_1.getRequest)(context);
44
+ if (!(0, utils_1.bearer)(request)) {
45
+ return true;
46
+ }
47
+ return this.strict.canActivate(context);
48
+ }
49
+ };
50
+ exports.OptionalJwtAuthGuard = OptionalJwtAuthGuard;
51
+ exports.OptionalJwtAuthGuard = OptionalJwtAuthGuard = tslib_1.__decorate([
52
+ (0, common_1.Injectable)(),
53
+ tslib_1.__metadata("design:paramtypes", [jwt_service_1.AuthTokenService])
54
+ ], OptionalJwtAuthGuard);
55
+ //# sourceMappingURL=optional-jwt-auth.guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optional-jwt-auth.guard.js","sourceRoot":"","sources":["../../src/auth/optional-jwt-auth.guard.ts"],"names":[],"mappings":";;;;AAAA,2CAA2E;AAC3E,qDAAgD;AAChD,+CAAiD;AACjD,mCAA6C;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACd,MAAM,CAAe;IAEtC,YAAY,MAAwB;QAClC,8EAA8E;QAC9E,wDAAwD;QACxD,IAAI,CAAC,MAAM,GAAG,IAAI,6BAAY,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,OAAO,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAA,cAAM,EAAC,OAAO,CAAC,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CACF,CAAA;AAlBY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;6CAIS,8BAAgB;GAHzB,oBAAoB,CAkBhC"}
@@ -0,0 +1,65 @@
1
+ import { AuthUser } from './auth.types';
2
+ /**
3
+ * Ownership assertions.
4
+ *
5
+ * `JwtAuthGuard` establishes *who* is calling. It cannot tell whether the
6
+ * records a request names belong to that caller — requests still carry
7
+ * `customerId`, `shopifyCustomerId` and `mobile` in their payloads, and a
8
+ * logged-in caller is free to put someone else's value there.
9
+ *
10
+ * These helpers close that gap: they compare a submitted value against the
11
+ * verified token and reject a mismatch. Call one at the top of every handler
12
+ * that accepts an identity value.
13
+ *
14
+ * A missing value is not a failure — many of these fields are optional, and
15
+ * "absent" means the handler has nothing to act on rather than an attempt to
16
+ * act on someone else. Validating that a required field is present stays the
17
+ * handler's own job.
18
+ */
19
+ /**
20
+ * Assert that a submitted customer identifier is the caller's own.
21
+ *
22
+ * Accepts either form Shopify uses — `"7654321"` and
23
+ * `"gid://shopify/Customer/7654321"` refer to the same customer and both pass.
24
+ *
25
+ * @throws {ForbiddenException} when the value belongs to another customer.
26
+ */
27
+ export declare function assertOwnsCustomer(user: AuthUser, submitted?: string | null): void;
28
+ /**
29
+ * For routes reachable by both guests and signed-in customers: if the request names a
30
+ * customer, require proof that the caller is that customer.
31
+ *
32
+ * Pairs with {@link OptionalJwtAuthGuard}, which leaves `user` undefined for a guest.
33
+ * The rule this encodes:
34
+ *
35
+ * - names no customer → allowed; the handler falls back to the guest `sessionId`
36
+ * - names a customer, no verified caller → 401, because the claim is unproven
37
+ * - names a customer that is not the caller's → 403
38
+ *
39
+ * Without this, an unauthenticated caller on an optional-auth route could name any
40
+ * customer and be served their data — exactly the hole the guard alone leaves open.
41
+ *
42
+ * The two failure codes differ because the client's next move differs: 401 means
43
+ * "authenticate and try again", 403 means "this is not yours, stop".
44
+ *
45
+ * @throws {UnauthorizedException} a customer is named but the caller is not
46
+ * authenticated.
47
+ * @throws {ForbiddenException} the named customer is somebody else.
48
+ */
49
+ export declare function assertOwnsClaimedCustomer(user: AuthUser | undefined, submitted?: string | null): void;
50
+ /**
51
+ * Assert that a submitted mobile number is the caller's own.
52
+ *
53
+ * Compares hashes, so the raw number never has to be held anywhere. Numbers are
54
+ * normalised first, so `"9876543210"`, `"+91 98765 43210"` and `"919876543210"`
55
+ * all match the same customer.
56
+ *
57
+ * A caller whose Shopify record has no phone number cannot pass this check —
58
+ * there is nothing to compare against, so any submitted number is rejected
59
+ * rather than waved through.
60
+ *
61
+ * @throws {ForbiddenException} when the number belongs to another customer, or
62
+ * when the caller has no phone number on record.
63
+ */
64
+ export declare function assertOwnsPhone(user: AuthUser, submitted?: string | null): void;
65
+ //# sourceMappingURL=ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../src/auth/ownership.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGvD;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,IAAI,CAaN;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,IAAI,CAaN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAwB/E"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertOwnsCustomer = assertOwnsCustomer;
4
+ exports.assertOwnsClaimedCustomer = assertOwnsClaimedCustomer;
5
+ exports.assertOwnsPhone = assertOwnsPhone;
6
+ const common_1 = require("@nestjs/common");
7
+ const logger_1 = require("../common/logger");
8
+ const auth_types_1 = require("./auth.types");
9
+ const utils_1 = require("./utils");
10
+ /**
11
+ * Ownership assertions.
12
+ *
13
+ * `JwtAuthGuard` establishes *who* is calling. It cannot tell whether the
14
+ * records a request names belong to that caller — requests still carry
15
+ * `customerId`, `shopifyCustomerId` and `mobile` in their payloads, and a
16
+ * logged-in caller is free to put someone else's value there.
17
+ *
18
+ * These helpers close that gap: they compare a submitted value against the
19
+ * verified token and reject a mismatch. Call one at the top of every handler
20
+ * that accepts an identity value.
21
+ *
22
+ * A missing value is not a failure — many of these fields are optional, and
23
+ * "absent" means the handler has nothing to act on rather than an attempt to
24
+ * act on someone else. Validating that a required field is present stays the
25
+ * handler's own job.
26
+ */
27
+ /**
28
+ * Assert that a submitted customer identifier is the caller's own.
29
+ *
30
+ * Accepts either form Shopify uses — `"7654321"` and
31
+ * `"gid://shopify/Customer/7654321"` refer to the same customer and both pass.
32
+ *
33
+ * @throws {ForbiddenException} when the value belongs to another customer.
34
+ */
35
+ function assertOwnsCustomer(user, submitted) {
36
+ if (submitted == null || submitted === '')
37
+ return;
38
+ if ((0, utils_1.normalizeGid)(submitted) !== (0, utils_1.normalizeGid)(user.customerId)) {
39
+ logger_1.logger.warn('Ownership check failed on customer id', {
40
+ callerCustomerId: user.customerId,
41
+ });
42
+ throw new common_1.ForbiddenException({
43
+ code: auth_types_1.AuthErrorCode.FORBIDDEN,
44
+ message: 'Not permitted',
45
+ });
46
+ }
47
+ }
48
+ /**
49
+ * For routes reachable by both guests and signed-in customers: if the request names a
50
+ * customer, require proof that the caller is that customer.
51
+ *
52
+ * Pairs with {@link OptionalJwtAuthGuard}, which leaves `user` undefined for a guest.
53
+ * The rule this encodes:
54
+ *
55
+ * - names no customer → allowed; the handler falls back to the guest `sessionId`
56
+ * - names a customer, no verified caller → 401, because the claim is unproven
57
+ * - names a customer that is not the caller's → 403
58
+ *
59
+ * Without this, an unauthenticated caller on an optional-auth route could name any
60
+ * customer and be served their data — exactly the hole the guard alone leaves open.
61
+ *
62
+ * The two failure codes differ because the client's next move differs: 401 means
63
+ * "authenticate and try again", 403 means "this is not yours, stop".
64
+ *
65
+ * @throws {UnauthorizedException} a customer is named but the caller is not
66
+ * authenticated.
67
+ * @throws {ForbiddenException} the named customer is somebody else.
68
+ */
69
+ function assertOwnsClaimedCustomer(user, submitted) {
70
+ if (submitted == null || submitted === '')
71
+ return;
72
+ if (!user) {
73
+ logger_1.logger.warn('Unauthenticated request named a customer id');
74
+ throw new common_1.UnauthorizedException({
75
+ code: auth_types_1.AuthErrorCode.TOKEN_MISSING,
76
+ message: 'Authentication required to act on a customer account',
77
+ });
78
+ }
79
+ assertOwnsCustomer(user, submitted);
80
+ }
81
+ /**
82
+ * Assert that a submitted mobile number is the caller's own.
83
+ *
84
+ * Compares hashes, so the raw number never has to be held anywhere. Numbers are
85
+ * normalised first, so `"9876543210"`, `"+91 98765 43210"` and `"919876543210"`
86
+ * all match the same customer.
87
+ *
88
+ * A caller whose Shopify record has no phone number cannot pass this check —
89
+ * there is nothing to compare against, so any submitted number is rejected
90
+ * rather than waved through.
91
+ *
92
+ * @throws {ForbiddenException} when the number belongs to another customer, or
93
+ * when the caller has no phone number on record.
94
+ */
95
+ function assertOwnsPhone(user, submitted) {
96
+ if (submitted == null || submitted === '')
97
+ return;
98
+ if (!user.phoneHash) {
99
+ logger_1.logger.warn('Ownership check failed: caller has no phone on record', {
100
+ callerCustomerId: user.customerId,
101
+ });
102
+ throw new common_1.ForbiddenException({
103
+ code: auth_types_1.AuthErrorCode.FORBIDDEN,
104
+ message: 'Not permitted',
105
+ });
106
+ }
107
+ if ((0, utils_1.hashPhone)(submitted) !== user.phoneHash) {
108
+ logger_1.logger.warn('Ownership check failed on mobile number', {
109
+ callerCustomerId: user.customerId,
110
+ });
111
+ throw new common_1.ForbiddenException({
112
+ code: auth_types_1.AuthErrorCode.FORBIDDEN,
113
+ message: 'Not permitted',
114
+ });
115
+ }
116
+ }
117
+ //# sourceMappingURL=ownership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.js","sourceRoot":"","sources":["../../src/auth/ownership.ts"],"names":[],"mappings":";;AA+BA,gDAgBC;AAuBD,8DAgBC;AAgBD,0CAwBC;AA9HD,2CAA2E;AAC3E,6CAA0C;AAC1C,6CAAuD;AACvD,mCAAkD;AAElD;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAChC,IAAc,EACd,SAAyB;IAEzB,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,IAAA,oBAAY,EAAC,SAAS,CAAC,KAAK,IAAA,oBAAY,EAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9D,eAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE;YACnD,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,yBAAyB,CACvC,IAA0B,EAC1B,SAAyB;IAEzB,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,eAAM,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;QAE3D,MAAM,IAAI,8BAAqB,CAAC;YAC9B,IAAI,EAAE,0BAAa,CAAC,aAAa;YACjC,OAAO,EAAE,sDAAsD;SAChE,CAAC,CAAC;IACL,CAAC;IAED,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAgB,eAAe,CAAC,IAAc,EAAE,SAAyB;IACvE,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,eAAM,CAAC,IAAI,CAAC,uDAAuD,EAAE;YACnE,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,IAAA,iBAAS,EAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5C,eAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE;YACrD,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { ExecutionContext } from '@nestjs/common';
2
+ /**
3
+ * Shape of the request object the guard reads from and writes to.
4
+ */
5
+ export interface AuthRequest {
6
+ headers: Record<string, string | string[] | undefined>;
7
+ user?: unknown;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * Get the underlying request, whether the handler is a REST controller or a
12
+ * GraphQL resolver.
13
+ *
14
+ * This has to branch. In a GraphQL execution context the request is not where
15
+ * `switchToHttp()` looks, and in an HTTP context `GqlExecutionContext.create()`
16
+ * does not produce a usable one. Calling either unconditionally works for one
17
+ * transport and silently misbehaves on the other.
18
+ */
19
+ export declare function getRequest(context: ExecutionContext): AuthRequest;
20
+ /**
21
+ * Extract a Bearer token from the `Authorization` header.
22
+ *
23
+ * Returns undefined when the header is absent or is not a Bearer credential —
24
+ * the caller decides whether that is an error.
25
+ */
26
+ export declare function bearer(request: AuthRequest): string | undefined;
27
+ /**
28
+ * Reduce a Shopify customer identifier to its numeric part so the two forms
29
+ * compare equal.
30
+ *
31
+ * Callers send either `"7654321"` or `"gid://shopify/Customer/7654321"`, and
32
+ * both refer to the same customer. Comparing the raw strings would reject a
33
+ * legitimate request.
34
+ */
35
+ export declare function normalizeGid(value: string): string;
36
+ /**
37
+ * Normalise an Indian mobile number to `91XXXXXXXXXX`.
38
+ *
39
+ * Mirrors the logic in the Shopify webhook handler so a number hashed at login
40
+ * matches the same number submitted in a request, whichever way it was typed:
41
+ * `"9876543210"`, `"+91 98765 43210"` and `"919876543210"` all converge.
42
+ */
43
+ export declare function normalizePhone(phone: string): string;
44
+ /**
45
+ * SHA-256 as lowercase hex.
46
+ */
47
+ export declare function sha256(value: string): string;
48
+ /**
49
+ * Hash a mobile number for storage in a token: normalise first, then hash, so
50
+ * the same number always produces the same digest.
51
+ */
52
+ export declare function hashPhone(phone: string): string;
53
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIlD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,WAAW,CAKjE;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAY/D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRequest = getRequest;
4
+ exports.bearer = bearer;
5
+ exports.normalizeGid = normalizeGid;
6
+ exports.normalizePhone = normalizePhone;
7
+ exports.sha256 = sha256;
8
+ exports.hashPhone = hashPhone;
9
+ const graphql_1 = require("@nestjs/graphql");
10
+ const crypto_1 = require("crypto");
11
+ /**
12
+ * Get the underlying request, whether the handler is a REST controller or a
13
+ * GraphQL resolver.
14
+ *
15
+ * This has to branch. In a GraphQL execution context the request is not where
16
+ * `switchToHttp()` looks, and in an HTTP context `GqlExecutionContext.create()`
17
+ * does not produce a usable one. Calling either unconditionally works for one
18
+ * transport and silently misbehaves on the other.
19
+ */
20
+ function getRequest(context) {
21
+ if (context.getType() === 'graphql') {
22
+ return graphql_1.GqlExecutionContext.create(context).getContext().req;
23
+ }
24
+ return context.switchToHttp().getRequest();
25
+ }
26
+ /**
27
+ * Extract a Bearer token from the `Authorization` header.
28
+ *
29
+ * Returns undefined when the header is absent or is not a Bearer credential —
30
+ * the caller decides whether that is an error.
31
+ */
32
+ function bearer(request) {
33
+ const raw = request.headers?.authorization ?? request.headers?.Authorization;
34
+ const header = Array.isArray(raw) ? raw[0] : raw;
35
+ if (!header)
36
+ return undefined;
37
+ const [scheme, token] = header.split(' ');
38
+ if (!scheme || scheme.toLowerCase() !== 'bearer' || !token) {
39
+ return undefined;
40
+ }
41
+ return token.trim() || undefined;
42
+ }
43
+ /**
44
+ * Reduce a Shopify customer identifier to its numeric part so the two forms
45
+ * compare equal.
46
+ *
47
+ * Callers send either `"7654321"` or `"gid://shopify/Customer/7654321"`, and
48
+ * both refer to the same customer. Comparing the raw strings would reject a
49
+ * legitimate request.
50
+ */
51
+ function normalizeGid(value) {
52
+ return String(value)
53
+ .trim()
54
+ .replace(/^gid:\/\/shopify\/Customer\//, '');
55
+ }
56
+ /**
57
+ * Normalise an Indian mobile number to `91XXXXXXXXXX`.
58
+ *
59
+ * Mirrors the logic in the Shopify webhook handler so a number hashed at login
60
+ * matches the same number submitted in a request, whichever way it was typed:
61
+ * `"9876543210"`, `"+91 98765 43210"` and `"919876543210"` all converge.
62
+ */
63
+ function normalizePhone(phone) {
64
+ const digits = String(phone).replace(/\D/g, '');
65
+ if (digits.startsWith('91') && digits.length === 12) {
66
+ return digits;
67
+ }
68
+ if (digits.length === 10) {
69
+ return `91${digits}`;
70
+ }
71
+ return digits;
72
+ }
73
+ /**
74
+ * SHA-256 as lowercase hex.
75
+ */
76
+ function sha256(value) {
77
+ return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
78
+ }
79
+ /**
80
+ * Hash a mobile number for storage in a token: normalise first, then hash, so
81
+ * the same number always produces the same digest.
82
+ */
83
+ function hashPhone(phone) {
84
+ return sha256(normalizePhone(phone));
85
+ }
86
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":";;AAsBA,gCAKC;AAQD,wBAYC;AAUD,oCAIC;AASD,wCAYC;AAKD,wBAEC;AAMD,8BAEC;AAhGD,6CAAsD;AACtD,mCAAoC;AAWpC;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,OAAyB;IAClD,IAAI,OAAO,CAAC,OAAO,EAAa,KAAK,SAAS,EAAE,CAAC;QAC/C,OAAO,6BAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC,GAAkB,CAAC;IAC7E,CAAC;IACD,OAAO,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAe,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,SAAgB,MAAM,CAAC,OAAoB;IACzC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC;IAC7E,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAEjD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;QAC3D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,KAAa;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC;SACjB,IAAI,EAAE;SACN,OAAO,CAAC,8BAA8B,EAAE,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,KAAa;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEhD,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACpD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACzB,OAAO,KAAK,MAAM,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,MAAM,CAAC,KAAa;IAClC,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,SAAgB,SAAS,CAAC,KAAa;IACrC,OAAO,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AACvC,CAAC"}
@@ -214,6 +214,9 @@ export interface CartAttributeInput {
214
214
  key: string;
215
215
  value: string;
216
216
  }
217
+ export interface CartCreateInput {
218
+ attributes?: CartAttributeInput[];
219
+ }
217
220
  export interface CommerceAppliedGiftCard {
218
221
  id: string;
219
222
  lastCharacters: string;
@@ -313,7 +316,7 @@ export interface ICommerceService {
313
316
  getCollectionByHandle(handle: string, productsFirst?: number): Promise<IntegrationResponse<CommerceCollection | null>>;
314
317
  getMenu(handle: string): Promise<IntegrationResponse<CommerceMenu | null>>;
315
318
  getPage(handle: string): Promise<IntegrationResponse<CommercePage | null>>;
316
- createCart(): Promise<IntegrationResponse<CommerceCart>>;
319
+ createCart(input?: CartCreateInput): Promise<IntegrationResponse<CommerceCart>>;
317
320
  getCart(cartId: string): Promise<IntegrationResponse<CommerceCart | null>>;
318
321
  addToCart(cartId: string, lines: CartLineInput[]): Promise<IntegrationResponse<CommerceCart>>;
319
322
  updateCartLines(cartId: string, lines: CartLineUpdateInput[]): Promise<IntegrationResponse<CommerceCart>>;